开心一刻

晚上,媳妇和儿子躺在沙发上

儿子疑惑的问道:妈妈,你为什么不去上班

媳妇:妈妈的人生目标是前20年靠父母养,后40年靠你爸爸养,再往后20年就靠你和妹妹养

儿子:我可养不起

媳妇:为什么

儿子:因为,呃...,我和你的想法一样

讲在前面

如果你们对 RabbitMQ 感到陌生,那可以停止往下阅读了

请先去查阅相关资料,对它有一个基本的了解之后再接着阅读本文

本文会以循序渐进的方式来讲解标题:

使用 RabbitMQ 的延迟队列来实现:订单在30分钟之内未支付则自动取消

所以请你们耐心逐步往下看

另外,实现标题的方式有很多,但本文只讲其中之一的 延迟队列,至于其他方式,不在本文讲解范围之内,如果想了解,烦请你们自行去查阅

消息何去何从

RabbitMQ 的模型架构,相信你们都知道

消息Producer 生成,经 Exchange 路由到 Queue ,然后推给 Consumer 进行消费

消费消息有两种方式

  1. 推模式(Basic.Consume)
  2. 拉模式(Basic.Get)

如果 消息Exchange 无法路由到符合条件的队列时,该 消息 该如何处理,是返还给 Producer 还是直接丢弃?

如果 消息 被路由到 Queue 时发现没有任何消费者,该 消息 该如何处理,是存在 Queue 中还是返还给 Producer ?

作为一个牛皮的中间件,一旦涉及到可选项了,应该怎么做?

我相信你们已经想到了,那肯定是增加配置参数来支持可选项嘛!

mandatory

mandatory 参数用于设置消息是否必须被路由到队列中,默认值是 false

mandatory 参数设置为 true 时,Exchange 无法根据自身的类型和路由键找到一个符合条件的 Queue,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃

mandatory 值为 false

代码执行正常,但没有输出结果,所以我们不确定消息是否投递了

但我们可以通过 RabbitMQ 管理界面,看 Exchange 概况

来确定消息确实投递了

mandatory 值为 true 时,需要添加一个监听器 ReturnListener

代码执行正常,同时也有输出结果

2024-06-01 14:54:52|AMQP Connection 10.5.108.226:5672|com.qsl.rabbit.PriorityMessageTest|INFO|59|Basic.Return 返回结果:mandatory test

也可以通过 RabbitMQ 管理界面,看 Exchange 概况来确定消息是否投递过

作为拓展,给你们留两个问题

  1. mandatory 设置为 true 的同时,不添加监听器 ReturnListener,会是什么结果
  2. mandatory 设置为 false 的同时,添加监听器 ReturnListener,又会是什么结果

immediate

immediate 参数用于设置消息是否立即发送给消费者,默认值是 false

immediate 参数设置为 true 时,如果消息路由到队列时发现队列上并没有任何消费者,那么该消息不会存入队列中,当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者

immediate 为 true ,消息路由到匹配的队列时

  1. 部分队列有消费者,有消费者的队列会立即将消息投递给消费者,没有消费者的队列会丢弃该消息
  2. 全部队列都没有消费者,则将该消息返回给生产者

执行如下代码

你会发现报错

2024-06-01 16:16:06|AMQP Connection 10.5.108.226:5672|org.springframework.amqp.rabbit.connection.CachingConnectionFactory|ERROR|1575|Channel shutdown: connection error; protocol method: #method<connection.close>(reply-code=540, reply-text=NOT_IMPLEMENTED - immediate=true, class-id=60, method-id=40)

这是因为从 RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持,对此官方解释如下

immediate 参数会影响镜像队列的性能,增加了代码复杂性,建议采用 TTLDLX 替代 immediate

概括来讲,mandatory 针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者。immediate 针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者

Alternate Exchange

生产者在发送消息时,如果不设置 mandatory 参数(或设置为 false),那么消息在未被路由的情况下会丢失;如果设置了 mandatory(且设置成 true),那么需要添加对应的 ReturnListener 逻辑,生产者的代码会变得复杂。如果既不想增加生产者的复杂,又不想消息丢失,那么就可以使用备份交换器(Alternate Exchange),将未被路由的消息存储在 RabbitMQ 中,在需要的时候再去处理这些消息

实现代码如下

执行如下测试代码

消息通过 com.qsl.normal.exchange ,经路由键 123 未匹配到任何队列,此时消息就会发送给 com.qsl.normal.exchange 的备份交换器 com.qsl.alternate.exchange,因为备份交换器的类型是 fanout,所以消息会被路由到 com.qsl.alternate.exchange 绑定的所有队列上,目前只有一个队列 com.qsl.unrouted.queue ,所以消息最终来到 com.qsl.unrouted.queue,消息流转如下

RabbitMQ 控制台看队列状况如下

备份交换器和普通的交换器没有太大的区别,为了方便使用,推荐选择 fanout 类型;你们也可以选择其他类型,比如 directtopic,但此时需要保证消息被重新路由到备份交换器的路由键和生产者发出的路由键是一样的,否则消息不能正确路由到备份交换器的队列中,消息会丢失!

关于备份交换器,以下几种特殊情况需要注意

  • 如果设置的备份交换器不存在,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
  • 如果备份交换器没有绑定任何队列,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
  • 如果备份交换器没有任何匹配的队列,客户端和 RabbitMQ 服务器都不会产生异常,此时消息丢失
  • 如果备份交换器和 mandatory 参数一起使用,mandatory 会失效

过期时长(TTL)

TTL,Time to Live 的简称,字面意思生存时长,也有很多人称过期时间,个人更习惯称过期时长

消息的 TTL

RabbitMQ 有两种方法对消息设置过期时长

  1. 通过队列属性设置,队列中的所有消息都有相同的过期时长
  2. 对消息本身进行单独设置,每条消息的过期时长可以不同

如果两种方法一起使用,则消息的过期时长以两者之间较小值为准(而非单纯的以消息的过期时长为准)

消息在队列中的生存时间一旦超过设置的过期时长,就会变成 死信(Dead Message) ,消费者将无法再通过正常的路由收到该消息

可以通过绑定 死信队列 来消费 Dead Message

通过队列属性 x-message-ttl 可以设置消息的过期时长,单位是毫秒,示例代码如下

如果不设置 TTL,消息不会过期;如果 TTL 设置成 0,则表示除非此时可以将消息直接投递给消费者,否则该消息直接被丢弃,这个特性是不是看起来很眼熟?回过头去看看 immediatetrue 时的第 1 个特性

1.部分队列有消费者,有消费者的队列会立即将消息投递给消费者,没有消费者的队列会丢弃该消息

通过参数 expiration 可以单独设置每个消息的过期时长,单位也是毫秒,示例代码如下

这两种方法的过期策略是怎样的,大家思考下再往下看

对于设置队列属性 x-message-ttl 的方法,队列中的消息具有相同的过期时长,队列中已过期的消息肯定是在队列头部,RabbitMQ 只需要定期的从队头开始往队尾扫描,一旦消息过期则从队列中剔除,一旦扫描到 未过期 的消息,则本次扫描完成

对于设置参数 expiration 的方法,每个消息可以设置不同的过期时长,那么过期的消息不一定在队列头部,如果要删除队列中所有过期消息,只能扫描整个队列,此时的成本是比较高的,所以采用惰性删除,即消息即将被投递给消费者时做过期判定,如果过期则进行删除

如果既设置了队列属性 x-message-ttl,又设置了 expiration,那该如何判定消息是否过期了呢?

定期删除 + 惰性删除,Redis 的过期策略是不是也是这个?

队列的 TTL

这里针对的是队列,而非队列中的消息,大家别和 消息的 TTL 搞混了

通过参数 x-expires 可以设置队列被自动删除前处于未使用状态的时长,单位是毫秒,不能设置为 0

未使用状态需要满足三点

  1. 队列上没有任何消费者
  2. 队列也没有被重新声明
  3. 过期时间段内未调用过 Basic.Get 命令

RabbitMQ 能保证在过期时长到达后将队列删除,但不保障及时。RabbitMQ 重启后,持久化的队列的过期时长会被重新计算

如下是创建一个过期时长为 30 分钟的队列

队列信息如下

死信队列

死信队列 之前,我们得先了解 DLX,全称 Dead-Letter-Exchange,中文翻译成 死信交换器

当消息在一个队列中变成死信之后

消息变成死信的情况包括以下3种

  1. 消息被决绝(Basic.Reject/Basic.Nack),并设置参数 requeuefalse
  2. 消息过期
  3. 队列达到最大长度

它能被重新发送到另一个交换器中,这个交换器就是 DLX,而绑定到 DLX 的队列就是 死信队列

DLX 也是一个正常的交换器,和一般的交换器没有区别,它可以和任何队列进行绑定,当绑定的队列中存在死信时,RabbitMQ 就会自动将这个消息重新发布到设置的 DLX 上,进而被路由到 死信队列死信队列 也是可以被监听的,也可以有消费者对 死信队列 中的消息进行消费处理的

所以,死信队列 可以变相的实现 immediatetrue 时的第 2 种情况

2.全部队列都没有消费者,则将该消息返回给生产者

为什么是 变相,因为不是直接将消息返回给生产者,而是生产者可以监听 死信队列 ,使消息回到生产者;虽然结果一致,但实现方式还是有区别的

那么 immediatetrue 的特性,就可以用 TTL + 死信队列 来替代了

通过参数 x-dead-letter-exchange 可以给队列添加 DLX;通过参数 x-dead-letter-routing-key 可以给这个 DLX 指定路由键,如果未配置该参数,则使用原队列的路由键,实现代码如下

执行如下测试代码

消息通过交换器 com.qsl.normal.exchange,经路由键 ttlMessage 匹配到队列 com.qsl.message.ttl.queue 中,队列设置了 x-message-ttl 为 3000 毫秒,这段时长内队列上没有消费者消费这条消息,消息过期。由于给队列设置了死信交换器 com.qsl.dlx.exchange,消息会通过该交换器,经路由键 dlx_routing_key 匹配到队列 com.qsl.dlx.queue 中,消息最终存储在该死信队列中,消息流转如下

RabbitMQ 控制台,可以看到队列状况如下

DLX 是一个非常有用的特性,它可以处理异常情况下,消息不能够被消费者正确消费而被置入到死信队列中,保证消息不被丢失;后续分析程序可以通过消费死信队列中的消息来分析当时所遇到的异常情况,进而改善和优化系统

DLX 还有一个很重要的功能,它配合 TTL 可以实现延迟队列,具体实现请往下看

延迟队列

延迟队列存储的对象是延迟消息

延迟消息 指的是需要延迟消费的消息

就是当消息发送之后,并不想让消费者立即拿到消息,而是等待特定时长后,消费者才拿到消息进行消费

延迟队列的使用场景有很多,例如:

  1. 订单系统中,下单完成之后 30 分钟内完成支付,否则取消订单
  2. 用户注册成功后,如果三天内没有登陆则进行短信提醒
  3. 远程控制扫地机器人,2 个小时后进行房间打扫
  4. ...

RabbitMQ 本身并没有直接支持 延迟队列 的功能,但是可以通过 DLXTTL 模拟出 延迟队列 的功能,具体实现已经在上一节(死信队列)中完成了,你们可以网上翻一翻

给大家演示 场景1 的完整示例,时间改成 1 分钟内完成支付

生产者端配置

消费者端配置

消息发送

输出日志如下

实际应用中,可以根据延迟时长给延迟队列划分多个等级,例如

目前 RabbitMQ 提供了另外的方式来实现 延迟队列

https://github.com/rabbitmq/rabbitmq-delayed-message-exchange

感兴趣的可以去看看

总结

  1. 示例代码:spring-boot-rabbitmq

  2. mandatory 与 immediate

    mandatory 针对的是消息能否路由到至少一个队列中,否则将消息返回给生产者

    immediate 针对的是消息能否立即投递给消费者,否则将消息直接返回给生产者,不用将消息存入队列而等待消费者

    RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持,可以用 DLXTTL 来代替

  3. 过期时长

    消息的过期时长有两种设置方式:队列的参数 x-message-ttl 和消息的参数 expiration

    队列也可以设置过期时长,该时长内队列一直处于未使用状态则会被删除;通过队列参数 x-expires 来设置

  4. 死信队列

    绑定到死信交换器(DLX)上的队列就是死信队列

    DLX 能够保证异常的情况下消息不会丢失,后续通过分析死信队列中的消息,可以改善和优化系统

  5. 延迟队列

    目前来讲,实现延迟队列的方式有两种

    1. DLXTTL
    2. rabbitmq-delayed-message-exchange

参考

《RabbitMQ实战指南》

RabbitMQ 进阶使用之延迟队列 → 订单在30分钟之内未支付则自动取消的更多相关文章

  1. Java 实现订单未支付超时自动取消

    在电商上购买商品后,如果在下单而又没有支付的情况下,一般提示30分钟完成支付,否则订单自动.比如在京东下单为完成支付: 超过24小时,就会自动取消订单,下面使用 Java 定时器实现超时取消订单功能. ...

  2. RabbitMQ 3.6.12延迟队列简单示例

    简介 延迟队列存储的消息是不希望被消费者立刻拿到的,而是等待特定时间后,消费者才能拿到这个消息进行消费.使用场景比较多,例如订单限时30分钟内支付,否则取消,再如分布式环境中每隔一段时间重复执行某操作 ...

  3. 基于rabbitMQ 消息延时队列方案 模拟电商超时未支付订单处理场景

    前言 传统处理超时订单 采取定时任务轮训数据库订单,并且批量处理.其弊端也是显而易见的:对服务器.数据库性会有很大的要求,并且当处理大量订单起来会很力不从心,而且实时性也不是特别好 当然传统的手法还可 ...

  4. 【RabbitMQ】一文带你搞定RabbitMQ延迟队列

    本文口味:鱼香肉丝   预计阅读:10分钟 一.说明 在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列.相信通过上一篇的学习,对于死信队列已经有了更 ...

  5. .Net之延迟队列

    介绍 具有队列的特性,再给它附加一个延迟消费队列消息的功能,也就是说可以指定队列中的消息在哪个时间点被消费. 使用场景 延时队列在项目中的应用还是比较多的,尤其像电商类平台: 订单成功后,在30分钟内 ...

  6. 【RabbitMQ 实战指南】一 延迟队列

    1.什么是延迟队列 延迟队列中存储延迟消息,延迟消息是指当消息被发送到队列中不会立即消费,而是等待一段时间后再消费该消息. 延迟队列很多应用场景,一个典型的应用场景是订单未支付超时取消,用户下单之后3 ...

  7. rabbitmq 实现延迟队列的两种方式

    原文地址:https://blog.csdn.net/u014308482/article/details/53036770 ps: 文章里面延迟队列=延时队列 什么是延迟队列 延迟队列存储的对象肯定 ...

  8. RabbitMQ如何实现延迟队列?(转)

    什么是延迟队列 延迟队列存储的对象肯定是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费. 场景一 ...

  9. Spring Boot(十四)RabbitMQ延迟队列

    一.前言 延迟队列的使用场景:1.未按时支付的订单,30分钟过期之后取消订单:2.给活跃度比较低的用户间隔N天之后推送消息,提高活跃度:3.过1分钟给新注册会员的用户,发送注册邮件等. 实现延迟队列的 ...

  10. RabbitMQ 延迟队列,消息延迟推送

    目录 应用场景 消息延迟推送的实现 测试结果 应用场景 目前常见的应用软件都有消息的延迟推送的影子,应用也极为广泛,例如: 淘宝七天自动确认收货.在我们签收商品后,物流系统会在七天后延时发送一个消息给 ...

随机推荐

  1. mysql 重新整理——索引优化explain字段介绍二 [十]

    前言 紧接上文. 正文 type type字段有如下类型: 1.all 2.index 3.rang 4.ref 5.eq_ref 6.const,system 7.null 最好到最差的顺序为: s ...

  2. 力扣591(java)-标签验证器(困难)

    题目: 给定一个表示代码片段的字符串,你需要实现一个验证器来解析这段代码,并返回它是否合法.合法的代码片段需要遵守以下的所有规则: 代码必须被合法的闭合标签包围.否则,代码是无效的. 闭合标签(不一定 ...

  3. PolarDB助力易仓打造跨境行业生态链协同的产业链SaaS

    简介: 2022年7月,易仓ECCANG WMS东南亚版正式上线!经过9年快速发展,易仓已经成为一家跨境全生态综合服务商,也见证了跨境行业从起步到万亿级的增长.而PolarDB作为面向下一代云原生关系 ...

  4. 性能提升 57% ,SMC-R 透明加速 TCP 实战解析 | 龙蜥技术

    ​简介:SMC-R 是如何加速 TCP 应用? 编者按:TCP 协议作为当前使用最为广泛的网络协议,场景遍布移动通信.数据中心等.对于数据中心场景,通过弹性 RDMA 实现高性能网络协议 SMC-R, ...

  5. 深入理解C++中的RVO

    前言 考虑存在这样一个类如HeavyObject,其拷贝赋值操作比较耗时,通常你在使用函数返回这个类的一个对象时会习惯使用哪一种方式?或者会根据具体场景选择某一种方式? // style 1 Heav ...

  6. Flink on Zeppelin 流计算处理最佳实践

    简介: 欢迎钉钉扫描文章底部二维码进入 EMR Studio 用户交流群 直接和讲师交流讨论~ 点击以下链接直接观看直播回放:https://developer.aliyun.com/live/247 ...

  7. 2018-8-10-使用-Resharper-快速做适配器

    title author date CreateTime categories 使用 Resharper 快速做适配器 lindexi 2018-08-10 19:16:51 +0800 2018-2 ...

  8. Etcd 可视化管理工具,GUI 客户端。

    Etcd Assistant--Etcd 可视化管理工具,GUI 客户端. 下载地址:http://www.redisant.cn/etcd 主要功能: 支持多标签页,同时连接到多个集群 以漂亮的格式 ...

  9. SQL Server实战二:创建、修改、复制、删除数据库表并加以数据处理

      本文介绍基于Microsoft SQL Server软件,实现数据库表的创建.修改.复制.删除与表数据处理的方法. 目录 1 交互式创建数据库表T 2 交互式创建数据库表S 3 T-SQL创建数据 ...

  10. 从原始边列表到邻接矩阵Python实现图数据处理的完整指南

    本文分享自华为云社区<从原始边列表到邻接矩阵Python实现图数据处理的完整指南>,作者: 柠檬味拥抱. 在图论和网络分析中,图是一种非常重要的数据结构,它由节点(或顶点)和连接这些节点的 ...