[TOC]
延迟处理事件
业务场景
此文,以自动关闭订单为例
下单后,未支付,自动关闭订单
Rocketmq 延迟队列
在 RocketMQ开源版本中,支持延迟消息
开源版本不支持任意时间精度的延迟消息,只支持特定级别的延迟消息
消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别
商业版本支持自定义时间
Rabbitmq 死信队列
Rabbitmq本身是没有延迟队列
使用 Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)实现延迟
死信交换机
死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机
并不是说死信交换机是某种特定的交换机
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。
TTL 到期
一个消息被Consumer拒收了,并且reject方法的参数里requeue是false
也就是说不会被再次放在队列里,被其他消费者使用。
上面的消息的TTL到了,消息过期,入死信交换机
队列溢出
队列的长度限制满了,排在前面的消息会被丢弃或者扔到死信路由上
消息TTL(消息存活时间)
RabbitMQ可以对队列和消息分别设置TTL
队列 TTL: 队列没有消费者连着的保留时间
TTL 选择
如果整个队列的所有消息都是相同的(同一业务场景),可以设置队列 TTL
一般设置消息 TTL,更灵活
落地方案
普通队列:delay_queue
存储订单,等待消息TTL 到期,投到 死信队列:dead_queue
死信队列:dead_queue
存储过期订单,等待被消费(进行订单关闭)
业务流程:
-
下订单,订单入普通队列 delay_queue,设置消息 TTL
-
不消费 delay_queue,等待 TLL到期,自动转入 dead_queue
-
监听 dead_queue,消费过期订单消息,关闭订单,ACK 手动确认消费状态
时间轮算法
-
创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)
-
环上每一个slot是一个Set (任务 Task 集合)
Task 有两个很重要的属性:
Cycle-Num:当 Current-Index第几圈扫描到这个Slot时,执行任务
业务ID:比如订单号
- 启动一个timer,这个timer每隔1s,Current-Index指针在环形队列中移动一格
入环形队列
下单后,根据 TTL,将订单号入环形队列
计算这个订单应该放在哪一个slot
- slot_index = ( ttl 秒数 - Current-Index ) % 环形队列.length
计算这个Task的Cycle-Num
- Cycle-Num = ttl 秒数 / 环形队列.length
Timer 执行逻辑
Current-Index每秒移动到一个新slot
这个 slot中对应的Set,看每个 Task的 Cycle-Num是不是0
如果不是0,说明还需要多移动几圈,将Cycle-Num减1
如果是0,说明马上要执行这个关单Task了
取出订单号执行关单(可以用单独的线程来执行Task)
并把这个订单信息从Set中删除
缺点
-
实现复杂,手写时间轮
-
需要额外考虑 持久化
Redis 过期监听
下单后,订单号入 Redis
入 Redis命令: set key value ttl
即: set order_[订单号] 订单号 600
开启 Redis Key过期事件通知
# 修改 redis.conf配置文件
# redis.conf 的默认的配置是:notify-keyspace-events ""
# 改为如下:
notify-keyspace-events Ex
# Ex 含义
# E Keyevent events, published with keyevent@ prefix
# x Expired events (events generated every time a key expires)
监听 Redis 过期事件
注入 RedisMessageListenerContainer
创建过期事件监听类
注入监听类(拿到带有订单号的 Key,进行订单关闭),继承 KeyExpirationEventMessageListener
缺点
不可靠:存在低延迟
官方指出:
基本上,过期事件是在 Redis服务器删除 Key时生成的,而不是在 TTL到期,立刻生成的
过期 Key删除,存在低延迟
Redis 删除过期 Key的三种策略
-
被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
-
主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
默认每秒运行 10 次
Redis 2.8: 可配,见 redis.conf
- 当前已用内存超过 maxmemory限定时,触发主动清理策略
分布式定时任务
两种常见任务存储方式:
- 任务入 DB
- 定时任务直接扫表
- 任务入 Redis
有序集合,时间戳作为 score
定时任务取指定时间区间的 score 记录
缺点
延时:不适用于高时效性的场景
即使调低任务时间间隔,基于业务量,会有性能压力