在之前的一篇文章中,简单介绍过有关订单超时关闭的基本情况。用户下完单,该订单已被创建但用户未支付,若超过规定时间内仍未付款,则关闭该订单并回滚库存及优惠券、积分等资源;若用户支付成功,则进入待发货状态,等待商家后续发货。除此之外,像用户下单后自动推送消息、商家发货后系统自动收货、用户签收后系统自动评价等等都是类似的业务场景。比较常见的到期关闭实现方式有如下几种:
- 定时任务
- 时间轮
- RabbitMQ死信队列
- RabbitMQ延迟消息插件
- RocketMQ延迟消息
- Redis过期监听
- Redission + Redis
定时任务
我相信绝大多数的开发者,第一时间想到的比较简单的到期关闭实现方式就是定时任务。原因也很简单,就是非常容易上手。实现原理就是通过调度平台根据设置的时间表达式(cron表达式)周期性的执行任务,扫描所有符合条件(到期)的数据记录,执行后续的业务处理。常用的像Timer、Spring Task,或者是Quartz、XXL-Job、ElasticJob、PowerJob等调度框架,又或者是SchedulerX(阿里的分布式任务调度平台)都能够实现定时任务的调度。但是,使用定时任务来实现关单操作还存在着一些问题:
1、时间不够精准。例如设置了5分钟一次定时扫表,这个时候有一笔订单即将超时,但是定时任务还没到执行时间,那么这笔订单的关单时间会比预期的时间要更晚一些。
2、定时扫表易对DB造成较大压力。设置的时间过短或者在某一时刻数据量非常大,就会让数据库负载过大甚至宕机,影响正常的业务。
3、分库分表带来的问题。人人都说分开好,分开麻烦少不了。数据量一上来,就想着分库或者分表,这个阶段使用定时任务去全表扫描数据,那么效率如何想必不用多说。
时间轮
❝
时间轮是一种机制,首次出现在论文《Hashed and Hierarchical Timing Wheels》中,简化了分布式定时器的实现。它是由多个格子组成,每个格子对应一个时间点,指针周期性的指向时间轮上的格子。当指针指向某一个格子时,时间轮会查找格子上维护的过期任务链表,然后执行所有任务。当前格子中的任务全部执行完成后,指针再指向下一个格子并且重复上述过程。
❞
使用时间轮方案效率更高,实现简单。不过缺点也比较明显,内存限制并且不适合集群扩展,适合单机的场景,分布式场景建议使用其它方案。
RabbitMQ死信队列
RabbitMQ,用过的最早的消息队列中间件。用它实现延时关单,主要原因是它的底层基于RabbitMQ的死信队列实现的。当RabbitMQ中的一条正常的消息,ttl过期或者被消费者拒绝等原因没办法被消费时,该消息会变成dead message(也就是死信)被发送到死信队列中。 开发者只要给某一条消息设置ttl过期时间,不去消费该消息,那么当过期时间触发,该消息就能够进入到死信队列中去。基于这种方式实现的延迟消息非常灵活而且高效,并且可以根据RabbitMQ集群扩展,实现高可用。
不过,需要注意的是,使用这种方式不代表没有问题。由于队列是先进先出的,每次判断时只会去判断队头是否过期,如果队头的消息时间很长一直不过期造成队头阻塞,后续队列中的消息已经过期了也没有被移除,一直阻塞下去。
RabbitMQ延迟消息插件
如果你想继续使用RabbitMQ来实现延时关单,但是又不想用ttl+死信队列,那么还有一种方案,就是使用官方的延迟消息插件(rabbitmq_delayed_message_exchange),能够很好的解决队头阻塞问题。实现原理就是,通过声明的x-delayed-message交换机,消息发布后不会立即进入到队列中,而是会被保存到基于Erlang开发的Mnesia数据库(分布式数据库管理系统)。通过一个定时器查询需要被投递的消息,如果消息过期,该消息会被x-delayed-type类型标记的交换机投递至目标队列进行消费。
优点: 不存在消息阻塞、可用性强且性能较好。
缺点:
- 最大支持延时时间约为49天,超过这个时间的消息会被立即消费;
- 超过百万的数据量不要使用(和RabbitMQ自身有关),内存和CPU使用量急剧上升;
- 需要确保版本兼容,不要使用过高版本的容易出现问题;
RocketMQ延迟消息
RocketMQ支持延迟消息,当延迟消息写入到broker不会被消费者立即消费,而是到达指定时长后才会被消费者消费处理。 RocketMQ的延迟默认不支持任意时长的(商业版除外,支持任意时长),支持1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h几个时长,大部分情况下这几个时长足够用了。如果不想购买商业版,又想使用RocketMQ的支持自定义时长的延迟消息,那么可以考虑使用RocketMQ5.0及以上版本,上述版本中增加了基于时间轮实现的定时消息,可以满足需求。
Redis过期监听
通过开启Redis消息监听配置,并实现KeyExpirationEventMessageListener,即可监听Redis中key的过期消息。不过这个方案不建议使用,主要原因是Redis官网明确指出,Redis不保证在Key过期的时候能立即被删除,也不保证这个消息能够被立即发出,也就是中间存在着一定延迟。当数据量很大时,这个延迟会很久。 详细可查看
Redission + Redis
在Redission中定义了延迟队列RDelayedQueue,基于zset实现的一个基于内存的延时队列,允许以指定延时时长将元素放到目标队列中。当我们需要增加数据到延时队列时,只需将数据和它所需的超时时间放到zset中,并开启一个延时任务。当任务到期时,再从zset中将数据取出,返回给客户端。感兴趣的朋友可以去看看RDelayedQueue的具体实现,你会了解的更多。
总结
以上就是关于订单超时关单的几种实现方案,每个方案都各有优缺点,需要根据自身实际情况(成本、复杂度、完整性等)来决定适合自己业务场景的方案。今天的分享就到这里,下篇文章再会~~