订单超时处理
JDK自带的延时队列
- 把订单插入DelayQueue中,以超时时间作为排序条件,将订单按照超时时间从小到大排序。
- 起一个线程不停轮询队列的头部,如果订单的超时时间到了,就出队进行超时处理,并更新订单状态到数据库中。
- 为了防止机器重启导致内存中的DelayQueue数据丢失,每次机器启动的时候,需要从数据库中
- 优点:简单,不需要第三方组件,成本低
- 缺点:
- 所有订单信息都要放入队列,内存占用大
- 没办法分布式处理,效率低
- 不适用订单量大的场景
RabbitMQ延时队列
RabbitMQ的延迟消息主要有两个解决方案
-
RabbitMQ Delayed Message Plugin
官方提供的延时消息插件,不是高可用的,节点挂了会丢失消息
-
消息TTL+死信Exchange
-
TTL:消息存活时间
RabbitMQ可以对队列和消息分别设置TTL,如果对队列设置,则该队列中所有的消息都会具有相同的过期时间。如果超过了这个时间,我们就认为这个消息是死信。
-
死信Exchange(DLX):死信交换机,在满足下面的条件时消息会进入该交换机
- 一个消息被Consumer拒收,并且Reject方法的参数是false。也就是说不会被放入队列,被其他消费者重试
- TTL到期的消息
- 队列满了被丢弃的消息
一个延时消息的流程如下图
-
优点:可以支持海量延时消息,支持分布式处理。
-
缺点:
-
- 不灵活,只能支持固定延时等级。
- 使用复杂,要配置一堆延时队列
RocketMQ的定时消息
RocketMQ支持任意秒级的定时消息,只需要在发送消息的时候设置延时时间即可。
- 优点:
- 精度高,支持任意时刻(付费版)
- 使用简单和普通消息一样
- 缺点:
- 使用限制,定时时长最大24小时。开源版本延迟时长固定18个级别
- 使用成本高,每个订单都会生成一个定时消息,不会马上消费,占用MQ的存储
- 同一时刻大量消息会导致消息延迟:定时消息的实现逻辑是通过时间轮算法,等定时时间到了以后才投递给消费者。如果同一时刻有大量消息需要处理,会照成系统压力过大,导致消息分发延迟,影响定时精度
Redis过期监听
-
redis配置文件开启"notify-keyspace-events Ex"
-
监听key的过期回调事件
var ret = ConnectionMultiplexer.Connect("127.0.0.1:6379,allowadmin=true"); IDatabase database = ret.GetDatabase(0); ISubscriber subscriber = ret.GetSubscriber(); subscriber.Subscribe("__keyevent@0__:expired", (channel, notificationType) => { Console.WriteLine(channel + "|" + notificationType); }); Console.ReadKey();
使用Redis进行订单超时处理的流程图如下
这个方案表面看起来没问题,但是在实际生产上不推荐。
Redis主要使用定期删除和惰性删除两种策略来清理过期的key
- 惰性删除:每次访问key的时候判断是否过期,如果过期就删除该key。若一个key过期了但是一直没有被访问,就会一直保存在数据库中
- 定期删除:每隔一段时间(默认100ms)就随机抽取一些设置了过期时间的key,检查其是否过期,如果有过期就删除。之所以这么做,是为了通过限制删除操作的执行时长和频率来减少对CPU的影响。
activeExpireCycle函数每次运行时,都从一定数量的数据库中随机取出一定数量的键进行检查,并删除其中的过期键
从上面的过程中可以发现,Redis过期删除是不精准的。Redis真正发起过期通知不是在key过期的时候,而是在key被删除的时候。如果在Redis发起通知的时候,应用重启或者崩溃了,通知事件就有可能丢失了,导致订单一直无法关闭,有稳定性问题。如果一定要使用Redis过期监听方案,还是需要配合定时任务做补偿机制。
定时任务分布式批处理
通过定时任务不断轮询数据库订单,将超时的订单分配给不同机器分布式处理。
定时任务的优点:
- 稳定性强:基于通知的方案(如MQ、Redis)需要考虑极端情况下的通知事件丢失的情况。定时任务只要保证业务幂等即可,哪怕这个批次有些订单没有处理或者处理过程中应用重启,下一个批次也可以继续处理,稳定性高。
- 效率高:基于通知的方案需要一个订单一个定时消息,consumer消费者处理的时候也需要一个一个订单处理,对数据库TPS很高。使用定时任务可以批处理,每次取出一批数据,处理完成后批量更新数据库。
- 可运维:基于数据库存储,可以很方便的对订单进行修改、暂停、删除等操作,如果业务执行失败也可以直接通过sql处理。
- 成本低:相对于其他业务需要第三方组件,复用数据库的成本大大降低。
定时任务的天然缺点就是无法保证高精度。定时任务的延迟时间由执行周期决定,最大可能会有2倍执行周期的误差。如果执行频率很高,就会导致数据库QPS过高,数据库压力太大,影响正常业务。所以一般需要抽离出超时中心和超时库来单独做订单的超时调度。
总结
如果对于超时精度要求比较高,超时时间在24小时内且没有峰值压力的场景,推荐使用RocketMQ的定时消息为解决方案。
如果对于超时精度没那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的批处理方案。
PS:通过两种方式来判断订单是否关闭,首先判断状态字段,如果状态不是关闭的,再判断订单创建时间和当前时间之间的时间差,如果符合关闭时间,就返回订单已关闭。可以在下次被定时任务处理时,将状态设置为关闭。