1.1、JDK延迟队列
该方案是利用JDK自带的java.util.concurrent包中的DelayQueue队列。
public class DelayQueue<E extends Delayed>extends AbstractQueue<E> implements BlockingQueue<E>
这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取原始,放入DelayQueue中的对象,必须实现Delayed接口
优点:
1.效率高
缺点:
1.JVM重启,数据会全部丢失
2.可扩展性难度高
3.可能出现内存溢出异常
4.内部很多东西可能需要开发人员手动编写,很多东西没有封装
1.2、定时任务
这种方式最简单,启动一个计划任务,每隔一定时间(假设1分钟)去扫描一次数据库,通过订单时间来判断是否超时,然后进行UPDATE或DELETE操作
优点:
1.实现简单
2.高可用,支持集群(Quartz\TBSchedule\XX-JOB\Elastic-Job\Staurm\LTS等)
缺点:
1.服务器内存消耗大
2.存在延迟,比如每一份扫描一次,延迟就是1分钟。也可能更久,比如1分钟之内有大量数据,1分钟没处理完,那么下一分钟就会顺延
3.效率低
4.数据库压力大,订单数据过大时,数据库压力也会增加
1.3被动取消
利用懒加载的思想,当用户或商户查询订单时,再判断该订单是否超时,超时则进行业务处理。
这种方式依赖于用户的查询操作触发,如果用户不进行查询订单操作,该订单就永远不会被取消。所以,实际应用中,也是"被动取消+定时任务"的组合方式来实现。这种情况下定时任务的时间可以设置的稍微‘长’一点
优点:
1.实现简单
2.支持集群(Quartz\TBSchedule\XX-JOB\Elastic-Job\Staurm\LTS等)
缺点:
1.产生额外影响,比如统计订单、统计库存等
2.影响用户体验,打开订单列表时需要处理数据,从而降低显示的实时性
1.4Redis Sorted Set
Redis有序集合(Sorted Set)每个元素都会关联一个double类型的分数score。Redis可以通过分数来为集合中的成员进行从小到大的排序。
该方案可以将订单超时时间戳与订单编号分别设置为score和member。系统扫描第一个元素判断是否超时,超时则进行业务处理。
然而,这一版本存在一个致命的硬伤,在高并发条件下,多个消费者会取到同一个订单编号,又需要编写Lua脚本保证原子性或使用分布式锁,用了分布式锁性能又下降了。
优点:
1.可靠性,基于Redis自身的持久化性实现消息持久化
2.高可用性,支持单价、主从、哨兵、集群多种模式
缺点:
1.单个有序集合无法支持太大的数量
2.需要额外进行Redis的维护
1.5、Redis事件通知
改方案是使用Redis的Keyspace Notifications,利用该机制可以在Key失效后,提供一个回调,实际上就是Redis会给客户端发送一个消息。需要Redis版本2.8以上。
优点:
1.可靠性,基于Redis自身的持久化特性实现消息持久化
2.高可用性,支持单击、主从、哨兵、集群多种模式
缺点:
1.开启键通知会对Redis产生额外的开销
2.目前键通知功能Redis并不保证消息必达,Redus客户端断开连接所以key会丢失
3.需要额外进行Redis的维护
1.6、时间轮算法
时间轮算法可以类比于时钟,按某一个方向固定频率轮动,每一个跳动称为一个tick。这样看出时间轮有3个重要的属性参数:
-
ticksPerWheel:一轮tick数
-
tickDuration:一个tick的持续时间
-
timeUnit:时间单位
当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的时钟走动完全类型。
优点:
-
效率高
-
如果使用Netty的HashedWheelTimer来实现,代码复杂比JDK的DelayQueue低
-
如果使用第三方中间件来实现,支持集群扩展,高吞吐量、消息持久化等。
缺点:
-
服务器重启后,数据全部丢失,怕宕机
-
集群扩展麻烦,难度较高
-
由于内存条件限制的原因,下单未付款的订单过多时,容易出现OOM异常
-
如果使用第三方中间件实现,需要额外进行第三方中间件的维护
-
1.7、RabbitMQ
1.7.1、延迟队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端为队尾,进行删除操作的端称为队头。
延迟队列,最重要的特性就体现在它的延时属性上,跟普通队列不一样的是,普通队列中的元素总是等着希望被早点取出消费,而延迟队列中的元素则是希望在指定时间被取出消费,所以延迟队列中的元素是都是带时间属性的。
简单来说,延迟队列就是用来存放需要在指定时间被处理的元素的队列。
本文使用RabbitiMQ也是通过延迟队列的机制来实现订单超时的处理。然而RabbitMg自身并没有延迟队列这个功能,实现该功能一般有以下两种方式:
-
利用TTL(Time To Live)和DLX(Dead Letter Exchanges)实现延迟队列
-
利用RabbitMQ的社区插件rabbitmq_delayed_message_exchange 实现
优点:
-
可靠性,消息持久化
-
高可用,非常方便部署负载均衡,实现高可用和吞吐量,轻松联合多个可用性区域和块
-
易管理和监控,使用HTTP-API,命令行工具或其他UI工具来管理和监控RabbitMQ
缺点:
-
系统可用性降低
-
系统复杂性变高
-
系统一致性问题
-
总结:
当然也要分实际情况来决定,如果贵司已经在用RabbitMg的情况下,延迟任务肯定首选使用RabbitMQ来实现,如果贵司并没有使用RabbitMQ,就为了实现这样一个功能而强行使用RabbitMg,在一个稳定运行的系统中引入一个第三方中间件是需要考虑很多问题的,否则就会得不偿失。
目前大型互联网公司多多少少都会引入消息中间件,毕竟它拥有解耦、异步、流量削峰、日志处理等优点及功能,是分布式系统中重要的组件。在这种情况下,使用消息中间件来实现延迟任务就变得理所当然了。
P:在实际项目中,还是要根据不同的业务需求以及各种方案的优缺点进行选择合适的方案,并不是性能最强就是最合适的。