Redis实现延迟队列(为订单超时关闭提供更多的解决方案)

电商场景中的问题向来很受面试官的青睐,因为业务场景大家都相对更熟悉,相关的问题也很有深度,也有代表性,能更方便地考察候选人的技术水平。

比如商品购买下单支付的流程,在买家购买商品后会先生成订单,之后有15或者30分钟的支付时间,如果超时未支付就会自动取消这个订单。

面试官:订单超时未支付自动取消,这个你用什么方案实现?

这里就不得不提延迟队列了,延迟队列是一种特殊的消息队列,它允许消息在特定时间点或延迟一段时间后才被消费者处理。这一特性使得系统能够更加灵活地控制任务的执行时机。延迟队列作为一种重要的消息队列模式,广泛应用于订单超时处理、定时任务处理、邮件延迟发送等场景。

延迟队列的实现方式多样,比如RocketMQ、RabbitMQ等消息队列本身就支持延迟队列的功能,如果公司正在用这些MQ组件,那可以直接使用。如果公司没有使用这些MQ组件,而在使用Redis,那么我们就可以考虑使用Redis实现延迟队列了。

Redis的Sorted Set数据结构天然适合实现延迟队列。可以将任务ID作为成员(member),任务的执行时间戳作为分数(score)。这样,通过ZADD命令可以轻松地按照执行时间将任务插入到集合中。而ZRangeByScore或ZRemRangeByScore命令则可以在合适的时机取出或删除已到期的任务。如下图:

图片

实现步骤主要包括:

1、任务入队:将任务详情序列化后存储,并以其执行时间戳作为score,通过ZADD命令加入到Sorted Set中。

2、任务出队:使用ZRANGEBYSCORE命令,配合WITHSCORES选项,获取当前时间戳之前的所有任务,并通过分数(score)判断哪些任务已经到期,然后进行处理。

3、周期性检查:通过启动一个额外的定时任务周期性检查并处理已到期的任务。

可能你会说,通过额外的定时任务检查还是挺麻烦的,是否可以使用Redis的Keyspace Notifications,订阅key过期事件来做?

答案是不可以。因为Redis的key过期事件并不能保证key过期的时刻能够及时发出通知事件,甚至不能保证key过期能发出事件。原因是,Redis删除过期key的时机是:客户端访问该key时Redis服务端发现过期或者Redis后台任务检测到这个key过期。如果一直不访问这个key,那有可能长期不能发现key过期,也就不会产生key过期的事件了。设置的key过期精确度如此不可控,这对于大部分使用延迟队列的业务场景应该是不可接受的。

实现生产可用的延迟队列还需要关注什么

按照上述的思路去具体实现一个延迟队列的话,还需要关注以下几点,这样才能打造出一个生产环境可用的好方案。

1、首先是性能。如果底层只采用一个Sorted Set,数据量大的时候,比如同时有几百万人下单,这些数据被存储到同一个Sorted Set,就容易引发性能瓶颈。可以采用指定数量的Sorted Set来解决此问题,这样生产和消费延迟消息的并发处理效率会提升。

2、其次是原子操作。在消费消息的时候可能涉及查询和删除的两步操作,有可能还涉及数据库等其他操作,如果部分处理失败,可能会造成消息丢失或者重复处理的问题。需要采用重试机制和幂等处理机制来应对。

3、最后是简单易用的封装。要实现好延迟队列,不是一件轻松的事儿。可设计上报延迟消息、到期回调处理两个接口,简化延迟队列的接入成本。可以参考Redission等封装实现,使用Sorted Set、消息Pub/Sub、Stream等结合实现完善的延迟队列。

具体实现Demo:

@Service
public class DelayOrderService {
    @Resource
    private RedisTemplate<String, String> redisTemplate;

    public void createOrder(String orderId, int timeoutSeconds) {
        long timeoutTimestamp = System.currentTimeMillis() + (timeoutSeconds * 1000);
        String orderKey = "order:" + orderId;
        redisTemplate.opsForZSet().add("orders_to_close", orderKey, timeoutTimestamp);
        System.out.println("Order " + orderId + " created with " + timeoutSeconds + " seconds timeout.");
    }

    @Scheduled(fixedDelay = 1000)
    public void checkAndCloseExpiredOrders() {
        long currentTime = System.currentTimeMillis();
        System.out.println("Checking for expired orders...Current time:" + currentTime);
        ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
        Set<String> allOrderKeys = zSetOps.range("orders_to_close", 0, -1);
        Set<String> filteredOrderKeys = allOrderKeys.stream()
                .filter(key -> key.startsWith("order:"))
                .collect(Collectors.toSet());
        for (String i : filteredOrderKeys) {
            double score = zSetOps.score("orders_to_close", i);
            System.out.println(i + ":" + score);
            //超时关闭
            if (score <= currentTime) {
                String orderId = i.substring("order:".length());
                System.out.println("Closing expired order: " + orderId);
                // 在这里处理关闭订单的业务逻辑,例如更新数据库状态
                // ...
                // 从Sorted Set中移除订单
                zSetOps.remove("orders_to_close", i);
            } else {
                System.out.println("Order " + i + " is still active.");
            }
        }
    }
}

@EnableAutoConfiguration
@RestController
@RequestMapping("/api/delay-order")
public class DelayOrder {

    @javax.annotation.Resource
    private DelayOrderService delayOrderService;

    @ApiOperation("新增")
    @RequestMapping(value = "/create/{time}", method = RequestMethod.GET)
    public Response<Boolean> create(@PathVariable int time) throws SimpleException {
        delayOrderService.createOrder(System.currentTimeMillis() + "", time);
        return Response.ok();
    }
}

补充:

ZSet代表Redis中的有序集合(Sorted Set)数据结构。它是一个集合,每个成员元素都会关联一个分数(score),Redis会根据这个分数对所有成员进行排序。因此,有序集合同时具备了集合和排序列表的特性:

  • 唯一性:集合中的每个成员都是唯一的,不允许重复。
  • 排序性:集合中的元素按照其分数进行排序,可以是升序或降序。有序集合支持的操作包括但不限于:
  • 添加元素并指定分数。
  • 根据分数范围或者成员排名来获取集合的子集。
  • 计算成员数量。
  • 增减成员的分数。
  • 获取指定成员的分数。
  • 删除指定成员等。

在Java中操作Redis有序集合时,通常通过如ZSetOperations这样的接口来进行。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值