领导:谁再用 Redis 过期监听实现关闭订单,立马滚蛋!
扫码关注“后端架构师”,选择“星标”公众号
重磅干货,第一时间送达!
责编:架构君 | 来源:cnblogs.com/Finley/p/16395466.html
责编:架构君 | 来源:cnblogs.com/Finley/p/16395466.html
上一篇好文:SpringBoot+Nacos+Kafka简单实现微服务流编排
大家好,我是后端架构师。
日前拜读阿牛老师的大作 领导:谁再用定时任务实现关闭订单,立马滚蛋![1] 发现其方案有若干瑕疵,特此抛砖引玉讨论一二。
在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?
一般实现的方法有几种:
使用 rocketmq、rabbitmq、pulsar 等消息队列的延时投递功能 使用 redisson 提供的 DelayedQueue
有一些方案虽然广为流传但存在着致命缺陷,不要用来实现延时任务
使用 redis 的过期监听 使用 rabbitmq 的死信队列 使用非持久化的时间轮
redis 过期监听
在 Redis 官方手册的keyspace-notifications
: timing-of-expired-events
中明确指出:
Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero
redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。redis 从未保证会在设定的过期时间立即删除并发送过期通知。实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见。
此外键空间通知采用的是发送即忘(fire and forget
)策略,并不像消息队列一样保证送达。当订阅事件的客户端会丢失所有在断线期间所有分发给它的事件。另外,搜索公众号Java就该这么学后台回复“面试”,获取一份惊喜礼包。
这是一种比定时扫描数据库更 “LOW” 的解决方案,请不要使用。
有另一位大佬做了测试 请勿过度依赖Redis的过期监听[2], 有兴趣的朋友可以自行查阅。
rabbitmq 死信
死信(Dead Letter
) 是 rabbitmq
提供的一种机制。当一条消息满足下列条件之一那么它会成为死信:
消息被否定确认(如channel.basicNack
) 并且此时requeue 属性被设置为false。 消息在队列的存活时间超过设置的TTL时间 消息队列的消息数量已经超过最大队列长度
若配置了死信队列,死信会被 rabbitmq 投到死信队列中。
在 rabbitmq 中创建死信队列的操作流程大概是:
创建一个交换机作为死信交换机 在业务队列中配置 x-dead-letter-exchange
和 x-dead-letter-routing-key
,将第一步的交换机设为业务队列的死信交换机 在死信交换机上创建队列,并监听此队列
死信队列的设计目的是为了存储没有被正常消费的消息,便于排查和重新投递。死信队列同样也没有对投递时间做出保证,在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。
为了解决这个问题,rabbit 官方推出了延迟投递插件 rabbitmq-delayed-message-exchange
,推荐使用官方插件来做延时消息。
这里说点题外话,使用 redis 过期监听或者 rabbitmq 死信队列做延时任务都是以设计者预想之外的方式使用中间件,这种出其不意必自毙的行为通常会存在某些隐患,比如缺乏一致性和可靠性保证,吞吐量较低、资源泄漏等。比较出名的一个事例是很多人使用 redis 的 list 作为消息队列,以致于最后作者看不下去写了 disque 并最后演变为 redis stream
。工作中还是尽量不要滥用中间件,用专业的组件做专业的事
时间轮
时间轮是一种很优秀的定时任务的数据结构,然而绝大多数时间轮实现是纯内存没有持久化的。运行时间轮的进程崩溃之后其中所有的任务都会灰飞烟灭,所以奉劝各位勇士谨慎使用。
redisson delayqueue
redisson delayqueue
是一种基于 redis zset
结构的延时队列实现。delayqueue
中有一个名为 timeoutSetName
的有序集合,其中元素的 score
为投递时间戳。delayqueue
会定时使用 zrangebyscore
扫描已到投递时间的消息,然后把它们移动到就绪消息列表中。
delayqueue
保证 redis 不崩溃的情况下不会丢失消息,在没有更好的解决方案时不妨一试。
在数据库索引设计良好的情况下,定时扫描数据库中未完成的订单产生的开销并没有想象中那么大。在使用 redisson delayqueue
等定时任务中间件时可以同时使用扫描数据库的方法作为补偿机制,避免中间件故障造成任务丢失。
结论
首先推荐使用 rocketmq
、pulsar
等拥有定时投递功能的消息队列。 在不方便获得专业消息队列时可以考虑使用 redisson delayqueue
等基于 redis 的延时队列方案,但要为 redis 崩溃等情况设计补偿保护机制。 在无法使用 redisson delayqueue
等方案时可以考虑使用时间轮。由于时间轮重启远比 redis 重启要频繁,定时扫库等保护机制更为重要。 永远不要使用 redis 过期监听实现定时任务。
参考:
https://juejin.cn/post/6987233263660040206
https://juejin.cn/post/684490415822759527
欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️
在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。
最后给读者整理了一份BAT大厂面试真题,需要的可扫码加微信备注:“面试”获取。
上一篇好文:SpringBoot+Nacos+Kafka简单实现微服务流编排
大家好,我是后端架构师。
日前拜读阿牛老师的大作 领导:谁再用定时任务实现关闭订单,立马滚蛋![1] 发现其方案有若干瑕疵,特此抛砖引玉讨论一二。
在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?
一般实现的方法有几种:
使用 rocketmq、rabbitmq、pulsar 等消息队列的延时投递功能 使用 redisson 提供的 DelayedQueue
有一些方案虽然广为流传但存在着致命缺陷,不要用来实现延时任务
使用 redis 的过期监听 使用 rabbitmq 的死信队列 使用非持久化的时间轮
redis 过期监听
在 Redis 官方手册的keyspace-notifications
: timing-of-expired-events
中明确指出:
Basically expired events are generated when the Redis server deletes the key and not when the time to live theoretically reaches the value of zero
redis 自动过期的实现方式是:定时任务离线扫描并删除部分过期键;在访问键时惰性检查是否过期并删除过期键。redis 从未保证会在设定的过期时间立即删除并发送过期通知。实际上,过期通知晚于设定的过期时间数分钟的情况也比较常见。
此外键空间通知采用的是发送即忘(fire and forget
)策略,并不像消息队列一样保证送达。当订阅事件的客户端会丢失所有在断线期间所有分发给它的事件。另外,搜索公众号Java就该这么学后台回复“面试”,获取一份惊喜礼包。
这是一种比定时扫描数据库更 “LOW” 的解决方案,请不要使用。
有另一位大佬做了测试 请勿过度依赖Redis的过期监听[2], 有兴趣的朋友可以自行查阅。
rabbitmq 死信
死信(Dead Letter
) 是 rabbitmq
提供的一种机制。当一条消息满足下列条件之一那么它会成为死信:
消息被否定确认(如 channel.basicNack
) 并且此时requeue 属性被设置为false。消息在队列的存活时间超过设置的TTL时间 消息队列的消息数量已经超过最大队列长度
若配置了死信队列,死信会被 rabbitmq 投到死信队列中。
在 rabbitmq 中创建死信队列的操作流程大概是:
创建一个交换机作为死信交换机 在业务队列中配置 x-dead-letter-exchange
和x-dead-letter-routing-key
,将第一步的交换机设为业务队列的死信交换机在死信交换机上创建队列,并监听此队列
死信队列的设计目的是为了存储没有被正常消费的消息,便于排查和重新投递。死信队列同样也没有对投递时间做出保证,在第一条消息成为死信之前,后面的消息即使过期也不会投递为死信。
为了解决这个问题,rabbit 官方推出了延迟投递插件 rabbitmq-delayed-message-exchange
,推荐使用官方插件来做延时消息。
这里说点题外话,使用 redis 过期监听或者 rabbitmq 死信队列做延时任务都是以设计者预想之外的方式使用中间件,这种出其不意必自毙的行为通常会存在某些隐患,比如缺乏一致性和可靠性保证,吞吐量较低、资源泄漏等。比较出名的一个事例是很多人使用 redis 的 list 作为消息队列,以致于最后作者看不下去写了 disque 并最后演变为
redis stream
。工作中还是尽量不要滥用中间件,用专业的组件做专业的事
时间轮
时间轮是一种很优秀的定时任务的数据结构,然而绝大多数时间轮实现是纯内存没有持久化的。运行时间轮的进程崩溃之后其中所有的任务都会灰飞烟灭,所以奉劝各位勇士谨慎使用。
redisson delayqueue
redisson delayqueue
是一种基于 redis zset
结构的延时队列实现。delayqueue
中有一个名为 timeoutSetName
的有序集合,其中元素的 score
为投递时间戳。delayqueue
会定时使用 zrangebyscore
扫描已到投递时间的消息,然后把它们移动到就绪消息列表中。
delayqueue
保证 redis 不崩溃的情况下不会丢失消息,在没有更好的解决方案时不妨一试。
在数据库索引设计良好的情况下,定时扫描数据库中未完成的订单产生的开销并没有想象中那么大。在使用 redisson delayqueue
等定时任务中间件时可以同时使用扫描数据库的方法作为补偿机制,避免中间件故障造成任务丢失。
结论
首先推荐使用 rocketmq
、pulsar
等拥有定时投递功能的消息队列。在不方便获得专业消息队列时可以考虑使用 redisson delayqueue
等基于 redis 的延时队列方案,但要为 redis 崩溃等情况设计补偿保护机制。在无法使用 redisson delayqueue
等方案时可以考虑使用时间轮。由于时间轮重启远比 redis 重启要频繁,定时扫库等保护机制更为重要。永远不要使用 redis 过期监听实现定时任务。
参考:
https://juejin.cn/post/6987233263660040206
https://juejin.cn/post/684490415822759527
欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️
在 GitHub猿 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!
PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。
最后给读者整理了一份BAT大厂面试真题,需要的可扫码加微信备注:“面试”获取。
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
END
最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。
别找了,想获取史上最全的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了
历史好文:
基于SpringBoot 的CMS系统,拿去开发企业官网真香
京东一面,面试官问我如何用 Nginx 禁止国外 IP 访问网站,我直接凉凉!
一套全部代码开源的快速开发平台,毫无保留给个人及企业免费使用
突发!腾讯QQ出现bug,不管原密码是什么,输入密码“123456789”就能登录!
扫码关注“后端架构师”,选择“星标”公众号
别找了,想获取史上最全的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了
历史好文:
基于SpringBoot 的CMS系统,拿去开发企业官网真香
京东一面,面试官问我如何用 Nginx 禁止国外 IP 访问网站,我直接凉凉!
一套全部代码开源的快速开发平台,毫无保留给个人及企业免费使用
突发!腾讯QQ出现bug,不管原密码是什么,输入密码“123456789”就能登录!
扫码关注“后端架构师”,选择“星标”公众号