ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

RabbitMQ延迟队列核心原理与生产实践

RabbitMQ延迟队列核心原理与生产实践 之前有个做交易系统的朋友问我用户下单 15 分钟内没支付就要自动关单还要顺带释放库存这个东西怎么设计最靠谱。他第一反应是写个定时任务每 30 秒扫一次订单表结果数据量一上来扫描成本直线上升真正要处理的异常单反而判断不出来。我给的方案是直接用 RabbitMQ 的延迟队列但这不是能随手拿来就用的东西。你得先弄明白 RabbitMQ 的架构和工作原理不然连“消息为什么没有按预期时间到达”这种问题都无从排查。这篇我会把 RabbitMQ 的组件模型、Exchange 路由规则、Channel 复用、消息确认与持久化机制一条条拆开讲再把重点放到延迟队列的完整实现上包括最常用的 TTL 加死信交换机方案、官方延迟插件方案最后补几个我在生产环境里真正踩过的坑。适合刚开始学消息中间件、或者正准备在项目里落地延迟任务的同学内容尽量做到能从原理一路跟到代码。1. 先认清 RabbitMQ 在整个消息链路里的角色一个邮局不只是一个队列很多教程上来就把 Producer、Consumer、Exchange、Queue 这几个概念列出来结果新手看完了只能记住名词真让他解释“为什么发消息不能直接发到队列里”一下就卡住了。其实把 RabbitMQ 理解成一个邮局系统就顺多了你写完信消息投进邮筒Exchange邮局根据地址routingKey和分拣规则Binding把信分发到对应的信箱Queue收信人Consumer再从信箱里取走。1.1 以“邮局”视角拆解消息流转链路拿真实业务举个例子用户下单后订单服务要告诉库存服务扣库存同时告诉积分服务加积分。如果订单服务直连每个下游服务的队列那每新增一个下游订单服务就得改代码、加配置而有了 Exchange订单服务只需要发一条消息到“订单事件交换机”由交换机按绑定关系把消息复制给多个队列就行。这就是解耦的价值。整个链路里的关键角色我习惯整理成一张对照表来记RabbitMQ 组件邮局类比核心职责Producer写信的人生产消息并发布到 ExchangeMessage信本身由 headers信封和 body内容组成Exchange邮筒和分拣台接收消息按类型和路由键匹配队列Binding邮编和分拣规则Exchange 与 Queue 之间的绑定关系Queue信箱消息真正存储和等待消费的地方Consumer收信人从队列拉取或订阅消息Virtual Host邮局里的独立分区隔离不同业务的消息空间Channel投递会话凭证在一个 TCP 连接上复用的虚拟通道这里面最容易忽略的是 Virtual Host。每个 vhost 拥有独立的 Exchange、Queue、Binding彼此之间完全隔离。我在公司里习惯按业务域拆分订单域一个 vhost、支付域一个 vhost、用户域一个 vhost权限也按 vhost 粒度控制出问题的时候互不影响排查范围也能一下缩小。1.2 四种 Exchange 的路由规则与真实使用场景RabbitMQ 支持四种交换机很多人只会用其中一两种其实每种都有明确的适用场景类型路由规则典型场景directroutingKey 完全匹配支付结果通知、余额变更fanout不认 routingKey广播到全部绑定队列订单创建后同时触发库存、积分、短信topic按通配符匹配 routingKey多级业务消息订阅headers按消息 headers 属性匹配极少用性能不如上面三种topic 的规则值得多说一句通配符*匹配一个单词#匹配零个或多个单词。假设某条消息的 routingKey 是order.created.v2那么绑定键order.#能匹配到order.*.v2也能匹配到而order.*匹配不到因为created.v2算两个单词。设计绑定键的时候我建议把业务域、事件名、版本号写成三段式后面做消息订阅扩展会非常省事。headers 类型我个人不太推荐。它的路由完全依赖消息属性不容易一眼看清消息流向调试成本高而且官方也建议优先用 direct/topic 代替。1.3 消息并不绑定在某一个队列上理解“路由”才能理解后面的延迟队列这里有个新手很难绕过来的弯一条消息被发到 Exchange 之后它并不属于任何队列而是由 Exchange 决定要不要放进某个队列。同一个消息可以被复制到多个队列也可以一个队列都进不去。消息在 RabbitMQ 里是和“路由键 属性 消息体”绑定的只有当某个队列通过 Binding 语义匹配上它消息才会被真正存储。这个认知对理解延迟队列极其重要。延迟队列的核心思路是消息先进一个正常队列“晾着”等它过期后再由交换机“转发”到另一个队列最终被消费者处理。本质上就是让一条消息连续经历两次路由中间靠 TTL 制造时间差。如果你脑子里始终是“消息发出去就直接进目标队列”的模型后面看死信转发会觉得别扭。2. 决定 RabbitMQ 能不能扛得住的几个机制你至少得搞懂一半架构图谱只是骨架真正影响系统稳定的是几个微观机制。我见过不少人把拓扑图画得漂漂亮亮一压测就出各种怪问题原因基本都集中在下面这几点。2.1 Channel一条 TCP 连接上的虚拟通道为什么要这样设计AMQP 协议里Connection 是真实的 TCP 连接Channel 是建立在 TCP 连接之上的虚拟通道。你可以把 Connection 理解成一条物理网线Channel 是网线里能同时跑的多个对话流。RabbitMQ 官方强烈建议一个连接里可以开很多 Channel多个线程不要共享同一个 Channel而应该各自维护一个。这么设计的理由是降低握手成本。如果每条消息都新建 TCP 连接TCP 三次握手加端口资源开销会让客户端性能大幅下降。我有一次压测时发现单台应用并发一上来系统出现大量 TIME_WAIT 连接后来把连接池改成了“每线程独立 Channel 复用 Connection”连接数瞬间从几千降到几十吞吐反而上来了。实际编码里Spring AMQP 的 RabbitTemplate 已经帮你管理了连接和 Channel 池普通业务不需要手动创建。但如果你在做底层封装记住一个原则Connection 要复用Channel 要按线程隔离不要跨线程共用否则会有意外的消息错乱。2.2 消息不丢靠三件事durable、persistent 和确认机制很多人听到“RabbitMQ 消息不会丢”就想当然其实这是有前提的。消息安全落地需要三个层面的配合第一Exchange 和 Queue 声明为 durable持久化。这意味着交换机和队列的元数据会写入磁盘RabbitMQ 重启后它们还在。只做这一步队列里的消息本身还保不住。第二消息设置 deliveryModepersistent。这是让消息体在进入队列后写入磁盘。Queue 持久化加消息持久化都做了重启才不至于数据清空。第三发布方要用 Publisher Confirm消费方要手动 ACK。Publisher Confirm 是发送端的确认消息被 RabbitMQ 正确路由并存储后会回一个确认给生产者消费端手动 ACK 则是确保消息处理成功后才从队列移除。我在生产环境见过一个事故队列声明没加 durableRabbitMQ 做版本升级重启后整个队列消失积压的几万条订单消息全没了下游对账一片红。自那以后我对持久化这三个开关的校验就像验合同一样逐条过。2.3 QoS 与 prefetch慢消费者会用光整个队列的脾气RabbitMQ 消费模型有个容易被低估的配置prefetch count预取数量。它决定消费者在收到 ack 确认前最多可以预取多少条消息。默认情况下某些客户端库的 prefetch 是 0也就是不限制消费者会疯狂拉消息堆积在本地内存里。假设你有两个消费者处理同一个队列消费者 A 处理速度快消费者 B 处理速度慢。如果不设 prefetchRabbitMQ 可能把大量消息先塞给 B结果 B 处理不过来A 还在闲着。这不是负载均衡而是典型的“一头堵死”。设置 prefetch 后每个消费者一次最多拿固定数量处理完一条再拿一条队列才能把压力分摊开。经验值是这样的处理时间几十毫秒的轻逻辑prefetch 可以放宽到 50 到 100处理时间几百毫秒甚至更久prefetch 建议控制在 5 以内。多消费者横向扩容时prefetch 设成 1 或 2 是最稳妥的起点。2.4 集群里常见的高可用方案从镜像队列到仲裁队列RabbitMQ 集群默认采用“节点互联 元数据复制”的方式。也就是说交换机、队列、绑定这些元数据会在集群各节点间同步但消息内容并不是每个节点都存一份。如果某个持有队列主副本的节点挂了这个队列就不可用了。要保证高可用必须启用队列复制。早期常用镜像队列所有镜像节点同步存储同一份消息主节点故障后可以从备份节点提升。但镜像队列在主从切换时存在脑裂风险而且实现机制比较重。RabbitMQ 3.8 以后官方推荐使用仲裁队列Quorum Queue它基于 Raft 协议实现消息强一致地存储在多个节点上节点故障后自动选主可靠性远高于旧版镜像模型。如果你在搭生产集群建议直接把队列类型声明成x-queue-typequorum。仲裁队列对消息持久化是强制要求这反过来说是个约束逼迫你不得不在数据安全层面做对。3. 把一条消息从发送到接收的完整旅程拆开看“架构和工作原理”这六个字最怕只停留在概念图。我建议你亲自打个断点看一条消息是怎么走完完整链路的。理解了这条链路遇到排查类问题就不至于瞎猜。3.1 一次标准投递的完整步骤我们拿“订单创建后发送一条扣库存消息”为例按顺序拆生产者创建 Channel声明 Exchange、Queue 以及两者的 Binding。实际生产中这些声明通常在应用启动时完成或者由运维预先建好。生产者发送消息到 Exchange消息里带着 routingKey 和 headers。Exchange 根据类型和绑定关系做路由匹配把消息放进匹配的 Queue。如果消息设置了持久化RabbitMQ 会把消息内容写入磁盘并在内存中维护索引。消费者通过basic.consume订阅队列RabbitMQ 按 prefetch 限制推送消息。消费者执行业务逻辑成功后发送basic.ack。RabbitMQ 收到 ack 后将消息标记为已确认并从队列中删除。看起来简单的七步每一步都可能出问题最常见的是第 3 步消息没有匹配到任何队列。如果没开 mandatory 参数这条消息会直接被丢弃开了 mandatory会通过 Return 回调返回给生产者让生产者感知“这条消息没送出去”。很多线上丢消息事故都是这个环节出的我在后面会专门展开。3.2 两种确认别搞混发送确认和消费确认作用完全不同RabbitMQ 里有两个 ack 概念新手特别容易混淆。第一个是发布者确认Publisher Confirm发生在生产者与 RabbitMQ 之间。当生产者把消息发到交换机RabbitMQ 成功接收并持久化后会异步返回一个 confirm。这个确认保证的是“消息已经安全交给 Broker”不保证消费者已经处理。第二个是消费者确认Consumer Ack发生在消费者与 RabbitMQ 之间。消费者拿到消息并处理完成后需要回复 ack如果处理失败可以回复 nack 并决定是否重新入队requeue。这个确认保证的是“消息已经被业务真正消费”。清理一下认知模型发布确认管“生产端不丢”消费确认管“消费端不丢”两者各有各的职责范围。生产级场景下我一般要求两端全开中间任何一环断了都能及时发现。3.3 消息不可达时的三种下场一条消息如果最终没被任何消费者正确处理会有三种去向。第一种是路由后找不到匹配队列直接丢弃除非开启 mandatory 让发送方收到 Return 回调。第二种是进入队列但一直没人消费积压占用内存和磁盘直到队列达到长度限制。第三种是被投递给消费者但消费者处理失败nack 且 requeuefalse此时消息会进入死信交换机DLX再由死信交换机转发到另一个死信队列。第三种就是延迟队列的底层机制。理解了这一点整个延迟队列的实现思路就串起来了消息先进入一个无人消费的等待队列按 TTL 配置等到过期过期后 RabbitMQ 自动把它丢给死信交换机再由死信交换机投递到真正处理业务的队列。整个过程不需要任何额外的定时器。4. 延迟队列的本质TTL 加死信交换机绕不开的经典组合现在正式进入今天的主菜。RabbitMQ 官方没有直接提供“延迟队列”这个原生类型但通过 TTL消息生存时间和 DLX死信交换机组合完全可以实现一个可靠、可控的延迟队列。这也是生产环境使用最广的方案。4.1 延迟到底延迟的是什么TTL 从哪一刻开始起算TTL 全称 Time To Live指的是消息存活的时长。RabbitMQ 有两种设置 TTL 的方式一种是在队列上声明x-message-ttl表示这个队列里的所有消息都拥有同样的过期时间另一种是在生产者发送消息时通过消息属性expiration逐条指定。两种方式可以并存以消息级的为准。很多人忽略一个关键细节TTL 从消息被放入队列的那一刻开始计时而不是从生产者发送那一刻开始。也就是说消息在进入等待队列之前经历的延迟不计算在 TTL 内。这在你做“消息发送时间 数据库处理耗时”叠加计算的时候要特别小心。另外RabbitMQ 对过期的判定是惰性检查。它不会为队列里的每条消息启动一个定时器而是当消息到达队列头部时才检查它的过期时间是否已到。这里有两个意义消息如果不在队列头部即使它的 TTL 已经到了也不会被立刻清除只有当它排队排到了头部RabbitMQ 才发现“这条该清理了”。这个机制直接导致我在 4.4 里说的那个经典坑。4.2 死信交换机是怎样把过期消息“换一条路”送出去的死信Dead Letter指的是那些最终没有被正常消费、按规则被放弃的消息。RabbitMQ 允许我们把这类消息重新发送到另一个交换机这个交换机就是死信交换机被转发的队列就叫死信队列。触发死信的来源有四种死信来源触发条件消息过期TTL 到期消息需要移除队列长度超限消息数超过队列 max-length被挤出消费者拒绝且不重新入队basic.reject 或 basic.nack 且 requeuefalse消息类型错误比如投递到 quorum queue 的消息属性非法延迟队列用到的是第一类消息过期。实际操作中业务要消费的队列是“死信队列”而生产者发送的目标是“等待队列”。等待队列在声明时通过两个参数指定死信去向x-dead-letter-exchange指向一个交换机x-dead-letter-routing-key指定死信消息路由到哪个队列的绑定键。需要留意的是死信消息在转发时不会原封不动RabbitMQ 会往消息的 headers 里添加一段x-death信息记录死亡原因、时间、原队列、原交换机等参数。排查问题时这段信息非常有用我建议把它打印到日志里省得靠猜。4.3 完整实现订单超时自动关闭Spring Boot 可直接抄直接给一套能跑的方案。场景是用户下单后 15 分钟不支付自动关单并标记超时。我们需要两个队列、两个交换机等待队列order.delay.queue绑定到order.delay.exchange声明死信交换机order.close.exchange死信路由键order.close最终队列order.close.queue绑定到order.close.exchange路由键order.close等待队列只做存储不做消费消息过期后自动转到最终队列由消费者执行关单。配置类代码如下Configuration public class RabbitDelayConfig { Bean public DirectExchange delayExchange() { return new DirectExchange(order.delay.exchange, true, false); } Bean public DirectExchange closeExchange() { return new DirectExchange(order.close.exchange, true, false); } Bean public Queue delayQueue() { MapString, Object args new HashMap(); // 关键配置消息过期后转发到哪个交换机 args.put(x-dead-letter-exchange, order.close.exchange); // 转发时使用的路由键 args.put(x-dead-letter-routing-key, order.close); return new Queue(order.delay.queue, true, false, false, args); } Bean public Queue closeQueue() { return new Queue(order.close.queue, true, false, false); } Bean public Binding delayBinding() { return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(order.delay); } Bean public Binding closeBinding() { return BindingBuilder.bind(closeQueue()).to(closeExchange()).with(order.close); } }生产者发送时通过消息属性的expiration设置延迟时间单位是毫秒public void sendDelayOrder(String orderId, long delayMillis) { MessageProperties props new MessageProperties(); // expiration 必须传字符串格式为毫秒值 props.setExpiration(String.valueOf(delayMillis)); props.setDeliveryMode(MessageDeliveryMode.PERSISTENT); Message message new Message(orderId.getBytes(StandardCharsets.UTF_8), props); rabbitTemplate.convertAndSend(order.delay.exchange, order.delay, message); }消费者监听最终队列执行关单逻辑RabbitListener(queues order.close.queue) public void handleClose(Message message, Channel channel) throws IOException { String orderId new String(message.getBody(), StandardCharsets.UTF_8); // 先查订单当前状态如果已支付则直接幂等返回 OrderEntity order orderMapper.selectById(orderId); if (order ! null OrderStatus.PAID.equals(order.getStatus())) { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // 执行超时关单、释放库存 closeOrderService.close(orderId); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); }注意一个关键点等待队列不能有消费者。一旦有人给等待队列加了消费者并手动 ack消息根本撑不到 TTL 就被消费了延迟逻辑直接被击穿。这个问题我在生产里真实遇到过后来靠监控队列消费者数量才及时发现。4.4 一个经典坑多条消息不同 TTL过期的不是你算好的那条这是 TTL 方案使用中最容易踩的坑我必须单独拿出来说。场景很简单等待队列里同时存在两条消息消息 A 的 TTL 是 5 分钟消息 B 的 TTL 是 10 分钟。消息 A 先进队列排在队列头部消息 B 后进排在 A 后面。10 分钟后你去查队列会发现消息 B 居然还留在队列里。原因是 RabbitMQ 的惰性过期机制只检查队列头部的消息而头部是消息 AA 在 5 分钟时已经过期被移走了此时 B 才排到头部它那 10 分钟从它到达头部那一刻才重新开始算。这不是 B 提前过期而是 B 被 A 的 TTL 延后了判定最终导致它的真实延迟时间变成了 15 分钟。这种问题在多条消息共用同一个等待队列、且各自设置了不同 expiration 时特别明显。如果你对延迟时间的精确性要求比较高就不要把不同 TTL 的消息混在同一个等待队列里。推荐的做法是按延迟级别拆分队列。15 分钟关单一个队列30 分钟提醒一个队列1 小时通知一个队列各自设置独立的x-message-ttl互不干扰。如果业务上延迟时间确实是任意的那就要接受这种近似延迟或者在消费侧根据x-death信息重新换算实际到期时间。5. 官方延迟插件x-delayed-message 用起来更简单但有不止一个限制TTL 加 DLX 方案能应对绝大多数场景但确实有个“时间不准”的天然缺陷。如果你需要更精准的延迟投递RabbitMQ 官方提供了一个插件rabbitmq_delayed_message_exchange对应的交换机类型是x-delayed-message。5.1 插件怎么工作怎么声明这个插件的工作原理是声明一个特殊的 Exchange 类型x-delayed-message消息发送进来后不会立即被路由而是先被 Exchange 暂存起来。每一条消息都携带一个x-delay参数单位毫秒表示延迟多久后再执行路由匹配。到达时间后插件把消息投递到绑定的队列完成延迟过程。RabbitMQ 3.12 版本开始官方已经把插件打包进了发行版启用命令很简单rabbitmq-plugins enable rabbitmq_delayed_message_exchange集群环境下每个节点都要执行一次。启用后可以在管理台看到创建交换机时可选x-delayed-message类型。Spring Boot 里声明这种交换机需要使用CustomExchangeBean public CustomExchange delayedExchange() { MapString, Object args new HashMap(); // 内部实际路由类型这里按 direct 处理 args.put(x-delayed-type, direct); return new CustomExchange(order.delayed.exchange, x-delayed-message, true, false, args); }生产者发送时Spring AMQP 提供了setDelay方法MessageProperties props new MessageProperties(); props.setDelay(15000); // 延迟 15 秒 Message message new Message(orderId.getBytes(StandardCharsets.UTF_8), props); rabbitTemplate.convertAndSend(order.delayed.exchange, order.close, message);从代码上看这个方案比 TTL DLX 简单很多不需要声明死信交换机也不需要等待队列生产者和消费者的心智负担小很多。5.2 插件和 TTLDLX 的真实差异精度、持久化、性能用插件不代表它全方面优于 TTL DLX。我把两种方案在几个维度上做了个对比实际选型时可以对着看对比维度TTL DLXx-delayed-message 插件延迟精度惰性检查头部消息阻塞时误差较大到点即投递精度更高持久化等待队列可持久化重启后消息可恢复延迟期消息只在 Exchange 内存中重启丢失实现复杂度需要声明死信交换机和等待队列只需一个特殊类型交换机延迟上限理论无上限但积压需要关注磁盘不适合长时间大规模延迟运维依赖原生能力需要插件升级恢复时要同步确认插件状态消息乱序同队列多 TTL 时受影响按 delay 到期顺序处理更可控插件方案最需要警惕的坑是数据安全。消息在延迟等待期间是存在交换机内存里的没有写入磁盘。如果此时节点重启这些消息会直接消失。我曾在测试环境验证过启用插件发 100 条延迟 10 分钟的消息立刻重启节点重启完成后 100 条全没了。而 TTL DLX 方案里的等待队列如果声明为持久化消息即使在延迟等待期也会落盘RabbitMQ 重启后消息还在只是到点后会继续按流程转发。所以如果业务对消息不能丢有硬性要求插件方案要非常谨慎除非你对节点稳定性有十足把握或者能接受极端情况下消息丢失后的兜底补偿。反过来如果延迟精度是第一位、而且你的服务具备幂等和补偿机制插件方案确实比 TTL DLX 省事不少。6. 延迟任务不只有消息队列一条路选型要看业务到底要什么写到这里可能会有人问既然 RabbitMQ 延迟队列有这些限制那我能不能不用它用别的方案当然可以。延迟任务的实现路径很多消息队列只是其中一条选型还是要回到业务需求本身。6.1 几种常见延迟方案对比方案优点缺点适合场景数据库定时轮询实现简单可靠扫描压力大、精度差、实时性低数据量小、延迟容忍度高Redis 过期键回调实时性好过期事件可能丢失集群下保障弱允许偶发丢失调度轻量时间轮内存精度高、吞吐大进程重启后任务全丢单机内部延迟调度RabbitMQ 延迟队列与 MQ 生态打通天然支持重试和确认精度受 TTL 机制影响长延迟积压成本高已有 MQ 基础、消息粒度延迟专业调度框架如 Quartz定时任务生态成熟任务粒度偏重不适合海量订单级延迟定时清点、批量处理我个人的选型倾向是如果延迟任务本身就是业务消息的一部分比如订单超时后要触发下游库存释放、发送通知那直接用 RabbitMQ 延迟队列最顺因为两端的业务链路天然是消息驱动的如果延迟任务是需要周期性扫描统计的“批处理”数据库加定时任务反而更简单可控。6.2 什么时候别硬上 RabbitMQ 延迟队列有几个场景我建议绕开 RabbitMQ第一延迟时间超长。比如延迟一天甚至几天消息在 Broker 里积压太久既占用磁盘又让队列长期处于水位告警状态故障恢复要重新堆积大量数据运维风险很高。这种情况下把“待执行任务”落库配合每天一次的定时任务扫描反而更轻量。第二要求毫秒级严格精确。TTL DLX 方案在头部阻塞时误差可能到分钟级插件方案精度更高但也达不到严格毫秒而且消息还可能丢失。真要这种精度建议用更专业的时序调度系统。第三大量消息同时到期。延迟队列的到期瞬间会产生突发流量比如几万条订单同时超时需要关闭消费者会被瞬时冲击。提前在消费端做好限流、分批和幂等否则延迟机制反而会把压力集中放大。我在做秒杀系统时就有过教训整点下单高峰产生几万条延迟消息30 分钟到期的订单集中触发消费者集群瞬间被打满。后来把最终队列的 prefetch 调低、按订单号做单机分组再配合降级策略才算扛住。最后聊几个我在实际项目里踩过的坑写代码容易写生产环境难延迟队列尤其如此。最后分享几个真实的教训希望对你有帮助。第一个坑是等待队列被误加消费者。TTL DLX 方案的等待队列理论上应该是“不设消费者”的但团队里新人接手时很容易顺手写一个RabbitListener绑定上去然后延迟队列瞬间变成普通队列消息全被提前处理。建议给等待队列的命名加上明确的标记比如xxx.delay.wait并在代码评审时特别强调这一条。第二个坑是死信消息的幂等。死信队列里的消息可能因为消费失败、nack、requeue 等被反复投递消费者必须对同一订单重复收到消息保持无感。我通常在消费者里“先查业务状态再执行动作”不做无脑关单。这个习惯不只针对延迟队列任何消息消费端都应该做成幂等。第三个坑是延迟消息的监控。队列里的消息积压不能被忽略尤其是等待队列一旦消息量异常增长说明发送端出了问题或者 TTL 配错了。我在监控面板上对等待队列加了深度告警对最终消费者加了消费延时的监控任何一方超过阈值都能及时被拉响。第四个教训是关于 TTL 的惰性检查。如果你用同一条等待队列承载多档延迟时间最终实际延迟会偏向“队首公告”。宁可拆成多档队列也不要贪图省事混在一个队列里。原理在前面已经讲过这里就不重复了。RabbitMQ 的延迟队列不是银弹但用对场景、踩掉坑它依然是我做过最顺手的消息延迟方案。你在实际项目里还遇到过哪些怪问题欢迎一起交流。
返回列表