开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情
在电商、支付等领域,经常会有这样的场景,用户下了订单然后放弃了支付,那订单一定会在规定的时间后关闭操作,细心的你一定已经发现像某宝、某东有这样的逻辑,而且时间非常准确,误差在1s之内;那么他们是如何做到的呢? 有几种通用的方法来实现这一点。
- 使用消息队列的延迟强制转换特性,如rocketmq、rabbitmq、pulsar等。
- 使用redisson提供的DelayedQueue。
有一些解决方案被广泛使用,但有致命的缺陷,不应该用于实现延迟任务。
- 使用redis过期的监听器。
- 使用rabbitmq的死信队列。 3.使用非持久时间轮。
Redis过期监控
在官方Redis手册keyspace-notifications: timing-of-expired-events中明确指出:
基本上,过期事件是在Redis服务器删除密钥时产生的,而不是在理论上的生存时间达到零时产生的
redis自动过期被实现为一个定时任务,离线扫描并删除部分过期的密钥;它会惰性地检查过期密钥,并在访问过期密钥时删除这些密钥。Redis不保证在设定的过期时间内立即删除和发送过期通知。事实上,过期通知比设置的过期时间晚几分钟是很常见的。
此外,密钥空间通知使用“触发并忘记”策略发送,并且不保证像消息队列那样传递。当客户端订阅某个事件时,它将丢失断开连接期间分发给它的所有事件。
这是一个比定时数据库扫描更“低”的解决方案,请不要使用它。 另一个伟大的人做了一个测试,请不要太依赖Redis的过期监听器,有兴趣的人可以试试。
RabbitMQ死信
死信是rabbitmq提供的一种机制。当消息满足下列条件之一时,它将变为死消息。
消息被否定(如在channel.basicNack中),requeue属性被设置为false。
消息在队列中存活的时间超过设置的TTL时间
消息队列中的消息数已超过最大队列长度
如果配置了死信队列,rabbitmq将死信转换到死信队列中。
在rabbitmq中创建死信队列的操作流程大致如下: 创建一个死信开关。 在业务队列中配置x-dead-letter-exchange和x-dead-letter-routing-key,并将步骤1中的交换机设置为业务队列的死信开关。 在死信开关上创建一个队列并监听该队列。
死消息队列用于存储未正确使用的消息,从而便于故障排除和重新交付它们。死亡消息队列在传递时间上也没有保证,直到第一个消息死亡,后续消息即使过期也不会作为死亡消息传递。
为了解决这个问题,rabbit推出了官方的延迟发布插件rabbitmq-delayed-message-exchange,建议使用官方的延迟消息插件。
作为旁注,使用redis过期侦听器或rabbitmq死消息队列用于延迟任务是在以一种设计人员没有打算的方式使用中间件,这种意想不到的行为通常有一定的缺陷,例如缺乏一致性和可靠性,低吞吐量和资源泄漏。一个著名的例子是,许多人使用redis列表作为消息队列,以至于作者最终写了disque,最终演变成redis流。
时间轮 对于定时任务来说,时间轮是一种优秀的数据结构,但是绝大多数时间轮的实现都是纯内存非持久化的。运行时间轮的进程将崩溃,其所有任务将化为乌有,因此建议战士谨慎使用它。
Redisson delayqueue Redisson delayqueue是一个基于redis zset结构的延迟队列实现。delayqueue有一个名为timeoutSetName的有序集合,其中元素的分数是传递时间戳。Delayqueue使用zrangebyscore定期扫描已经到达传递时间的消息,然后将它们移动到就绪消息列表中。
delayqueue保证redis不会丢失消息而不会崩溃,在没有更好的解决方案的情况下值得一试。
结论
首先建议使用rocketmq、pulsar和其他具有定时传递功能的消息队列。 在访问专业消息队列不方便的情况下,可以考虑使用redissondelayqueue等基于redis的延迟队列方案,但要针对redis崩溃等情况设计补偿保护机制。 在无法使用redissondelayqueue等程序时可以考虑使用时间轮,因为时间轮重启的频率远比redis重启的频率高,定时扫描库等保护机制更为重要。 永远不要使用redis过期监听器来实现定时任务。
使用设计良好的数据库索引,定期扫描数据库查找未完成订单所引起的开销不会像人们想象的那么大。定时任务中间件(如redisson delayqueue)可以与扫描数据库一起使用,作为一种补偿机制,以避免由于中间件故障而导致的任务丢失。