资讯详情

RocketMQ事务消息生产落地:回查、监控与死信处理实战细节

📅 2026/10/14 11:42:36 | 华诺云谱 👁 阅读
RocketMQ事务消息生产落地:回查、监控与死信处理实战细节
做RocketMQ事务消息很多人以为把半消息发出去、本地事务执行完、再提交或回滚就万事大吉了。实际跑了几个高并发项目之后我发现真正决定事务消息能不能稳定扛住线上流量的往往是那些文档里一笔带过、或压根没写的细节回查线程池隔离、半消息指标监控、状态表设计、死信和堆积的处理。这篇延续系列第九篇重点补上事务消息在生产落地时的实战细节大部分内容来自我在压测和线上问题排查中的直接经验不是原理复述。1. 为什么事务消息写完本地事务还不够回查机制的隐藏成本事务消息的核心价值是解决本地事务和消息发送不一致的问题先发半消息再执行本地事务最后根据本地事务结果提交或回滚半消息。这套流程看起来简单但它的可靠性完全建立在回查机制上。所谓回查就是当半消息一直等不到明确的提交或回滚指令时Broker会定期回调生产者的checkLocalTransaction接口向业务侧确认本地事务的真实状态。这里有个很重要的认知回查不是低频事件而是常态路径。我见过一个订单系统每天事务消息量在百万级别其中约5%~10%的消息会进入回查流程。按这个比例每秒就有几十到上百次回查请求打向生产者应用。回查接口如果每次都要查数据库对这个数据库连接池和响应时延的压力和核心业务接口没什么区别。回查的主要触发场景有三种生产者发送半消息后本地事务执行时间过长超过了Broker等待提交指令的时间窗口。生产者发送半消息后应用进程在提交或回滚前崩溃半消息成了无主消息。网络抖动导致提交或回滚指令丢失Broker只能靠回查兜底。回查频率由Broker侧的transactionCheckInterval控制默认是60秒transactionCheckMax默认15次。换句话说一条半消息最多会被回查15次第一次在60秒后之后每隔60秒一次。如果回查接口每次都返回UNKNOW这条消息会在15次之后被转入死信队列。所以使用事务消息时不能只关注发送和提交的路径更要对回查接口的性能、正确性和可用性做专项保障。我建议至少做到三点回查接口必须轻量核心逻辑就是查本地事务状态表不要查一堆外部依赖数据库连接池要给回查留出独立额度避免和核心业务接口争抢回查接口要做好监控P99耗时超过500毫秒就要告警。1.1 回查随意写业务逻辑很容易拖垮本地事务接口很多团队在写checkLocalTransaction时习惯把回查做成一个万能方法先查本地事务状态表再查关联业务表如果关联数据不完整还要调用远程服务补齐状态。这样做表面上让回查结果变得更准确实际上是把回查变成了一个重量级操作。RocketMQ的回查是阻塞同步的Broker发起回查要等生产者返回明确结果后才决定这条半消息是提交、回滚还是继续等待。如果回查接口的平均耗时从20毫秒上升到500毫秒Broker在等待回查结果期间会占用大量的网络连接和调度线程资源。在消息量大的集群中回查接口变慢甚至可能影响Broker对其他正常消息的投递。所以回查方法里不要做远程调用不要做耗时超过50毫秒的重计算原则上只做一件事查本地事务状态表把状态映射为回查结果。如果状态表查不到记录宁可返回ROLLBACK_MESSAGE或UNKNOW也不要去做远程补偿查询。1.2 事务状态上报的原子性与半消息状态机事务消息在Broker侧的状态流转是半消息(Pending) - 等待回查(NeedCheck) - 提交(Committed)或回滚(Rollback)。业务侧要配合这个状态机就要在本地事务中同时维护事务状态记录和业务数据两者必须原子提交。我用得比较顺手的做法是在业务主表旁边建一张transaction_record表事务消息发送前先生成一条状态为执行中的记录本地业务逻辑执行成功后把这条记录更新为已提交然后调用commit如果本地业务逻辑抛异常把记录更新为已回滚然后调用rollback。这里的关键是transaction_record和业务主表必须在同一个本地事务里提交否则就会出现业务数据已提交、事务状态还是执行中的情况回查接口永远返回UNKNOW消息最终变成死信。有人会问不是应该先发半消息再执行本地事务吗那本地事务的记录怎么生成顺序可以这样设计先发送半消息拿到transactionId然后在本地事务里插入transaction_record并更新业务数据事务提交成功后再拿着transactionId去调用commit。如果本地事务提交失败则调用rollback。这样transaction_record的写入和业务数据写入天然保持原子性。2. 半消息与死信队列的日常监控我建议盯紧这三个指标事务消息在线上运行最怕的是半消息只增不减或者死信队列悄悄膨胀。这两个问题平时可能不显眼一旦累积到足够量级轻则消息积压重则业务数据不一致。监控指标我习惯集中在三个半消息数量、回查次数、死信数量。半消息数量在RocketMQ Dashboard里没有现成的面板但可以通过Broker的RMQ_SYS_TRANS_HALF_TOPIC这个内部Topic的消息量来观测。这个Topic专门保存所有半消息正常情况下它的消息量应该在一个相对稳定的区间波动不会持续上涨。如果发现半消息数量持续上升说明有大量本地事务没有及时提交或回滚需要马上检查生产者的本地事务执行链路。回查次数可以通过Broker的transactionCheck相关日志统计也可以在checkLocalTransaction接口埋点监控。回查次数的异常上涨往往意味着本地事务执行耗时变长或者提交指令丢失的概率变大。死信数量可以按消费组维度观察每个消费组的DLQ_前缀Topic就是死信队列。死信消息分为两类一类是普通消费重试次数耗尽之后转入的另一类是事务消息回查次数耗尽之后转入的。处理方式不同前者优先排查消费逻辑后者优先排查回查接口和本地事务状态表。监控时最好能把这两类死信分开标记可以用消息的property区分否则排查起来容易抓瞎。2.1 死信消息的消费重试策略与报警联动死信队列不应该只是默默堆积。我通常会给死信队列配置一个监听程序每分钟扫描一次死信消息数量一旦超过阈值就触发告警并把死信消息体完整保留到日志或对象存储中方便后续追踪。设计的重试策略也很重要。死信消息一般不会直接删掉而是通过定时任务重新投递到原Topic或一个专门的恢复Topic。重试投递要注意幂等性消息重新投递后消费端必须根据业务主键做去重否则会出现重复处理。比如一条扣款消息如果消费端没有幂等重新投递一次就可能重复扣款。另一种做法是把死信消息人工介入处理拉取死信消息列表逐个检查本地事务状态表确认事务状态后再决定是重新投递还是丢弃。这样最稳因为死信消息本身就是异常信号自动重试虽然省事但可能掩盖真正的bug。2.2 事务消息的位移与消费者组偏移量关系事务消息有个容易让人困惑的地方半消息虽然存在RMQ_SYS_TRANS_HALF_TOPIC但普通消费者不会直接看到它。只有半消息被提交后才会从半消息队列转移到真正的业务Topic队列中消费者才能拉取到。这意味着在消息提交之前RMQ_SYS_TRANS_HALF_TOPIC的消费位点会一直停在不动的状态不要把这个当成消息堆积。之前排查过一个case某消费组订阅了业务Topic正常消费没有异常但Dashboard显示这个Topic的消息堆积量持续上升。查了半天才发现堆积量统计把RMQ_SYS_TRANS_HALF_TOPIC的位点也算进去了而半消息队列长期不被消费位点自然一直落后。这个不是故障是统计口径的问题。遇到类似情况先确认堆积的队列是不是半消息队列再决定是否处理。半消息转正后消息的bornTimestamp保持不变但storeTimestamp会更新。消费者如果需要区分这条消息是正常发送的还是事务消息转正而来的可以通过消息属性中的TRAN_MSG标记判断。这个标记只有在事务消息被提交后才会出现在业务消息的属性里可以用于统计事务消息的比例但不能用于业务逻辑判断因为它的存在只是为了内部追踪。3. 事务消息生产环境告警配置从阈值到指标维度的落地告警配置是事务消息落地中最容易被忽略的环节。很多团队把注意力放在发送、消费、回查接口上却忘了给事务消息的消息量、半消息数、死信数配置告警导致问题积累到爆发时才被发现。我这里的建议是配置四类核心告警分别对应消息量异常、消费堆积、事务回查异常、死信堆积。3.1 消息量突增与突降的双向告警事务消息的量级相对稳定除非有大促或活动。但消息量出现突增时要警惕是否是某些非预期逻辑在疯狂发送事务消息消息量突降时要警惕是否发送链路出了问题比如生产者异常退出了。我习惯对以下三个指标配置告警生产者发送TPS超过正常基线2倍或低于正常基线50%都需要告警。事务提交比例事务提交数除以事务消息总数这个比例长期低于70%时需要检查本地事务成功率。回查次数回查次数突然上涨30%以上往往意味着本地事务执行异常。告警阈值不要写死要根据每个业务的历史基线动态调整。比如某业务平时事务消息TPS是500大促时能冲到5000如果阈值写死为1000大促时就会产生大量误报。建议先用一段时间的历史数据训练基线再按基线的倍数设置阈值。3.2 消费堆积指标的阈值设置与动态调优消费堆积是事务消息最常遇到的告警类型。堆积产生的原因多种多样消费者处理能力不足、消费逻辑出现阻塞、消费者线程池配置不合理。RocketMQ Dashboard中消费堆积相关的指标主要有两个diff和accumulative。diff是当前消息积压量accumulative是累计积压量。监控diff时我通常关注它的增长速率而不是绝对值。比如某个消费组平时diff维持在1000以内某次压测后突然增长到10万并且持续增长说明消费速度远低于生产速度这时候即使绝对值还没达到告警阈值也应该触发告警因为增长趋势已经暴露了问题。消费者线程池配置方面consumeThreadMin和consumeThreadMax决定消费线程的规模。理论上这两个值越大消费能力越强但实际中线程过多会导致线程切换开销增大反而降低吞吐。我习惯先设置consumeThreadMin20、consumeThreadMax64然后根据压测结果调整。如果发现消费吞吐上不去优先考虑增加Topic的队列数而不是无脑加大线程数因为RocketMQ中单个队列的消费是串行的线程再多也突破不了队列数的并发上限。3.3 事务回查场景的告警指标事务回查相关的告警建议关注半消息队列的堆积量和回查失败的次数。半消息队列堆积量可以从RMQ_SYS_TRANS_HALF_TOPIC的消息量获取如果这个量持续上涨说明大量本地事务长时间未提交或回滚需要排查。回查失败次数可以从两个维度统计一是Broker端日志中回查超时或回查失败的次数二是生产者端checkLocalTransaction接口的异常次数。回查失败次数的增长往往意味着本地事务状态表查询异常或者事务状态长时间未更新。4. 回查二次补偿失败场景对账事务状态与B端补偿耗时接下来聊一个生产环境里真实踩过的坑本地事务成功提交了但事务消息的回查一直失败最终消息进入死信队列B端永远收不到通知。这个场景的根因通常不是RocketMQ本身而是本地事务状态表和回查接口没有做好配合。场景描述A系统通过事务消息通知B系统做后续处理。A系统发送半消息后执行本地事务本地事务提交成功但在提交事务消息指令发出前A系统进程崩溃。之后Broker发起回查A系统的回查接口应该查询本地事务状态表并返回COMMIT_MESSAGE。但问题是A系统的本地事务状态表根本没有记录这条事务回查接口查不到任何东西只能返回UNKNOW。回查次数最终耗尽消息进入死信队列。这种情况发生的原因往往是本地事务状态表的写入和业务数据不在同一个事务中。如果本地事务状态表晚于业务数据提交甚至根本不维护事务状态表发生崩溃时业务数据已提交但事务状态表还没有记录回查就失去了依据。另外B端消费的耗时也可能是回查失败的原因。事务消息消费是异步的B端消费耗时越长消费者位点推进越慢消息堆积量就越大。如果B端消费逻辑中存在远程调用、批量处理等耗时操作事务消息的消费堆积会显著上升。建议B端消费逻辑中把耗时操作异步化保证消费流程轻量。4.1 回查接口返回值的规范性陷阱回查接口checkLocalTransaction的返回值有三种COMMIT_MESSAGE、ROLLBACK_MESSAGE、UNKNOW。其中UNKNOW代表不确定请稍后再查。这个返回值本身没问题问题在于使用方式。很多团队在回查接口中把所有异常都返回UNKNOW比如查不到事务状态返回UNKNOW、数据库连接失败返回UNKNOW、参数解析失败返回UNKNOW。这样做看起来是保守实际上是灾难。因为RocketMQ的回查次数有限每次UNKNOW都会消耗一次回查机会最终结果就是消息进入死信队列。正确的做法是查得到明确状态就返回明确结果只有真正处于本地事务还在执行中时才返回UNKNOW。其他异常情况该返回ROLLBACK就返回ROLLBACK不要滥用UNKNOW。4.2 本地事务状态表的设计既然本地事务状态表是回查的依据它的设计就非常关键。我常用的表结构大概是这样字段类型说明tx_idvarchar(64)全局事务ID对应RocketMQ的transactionIdbiz_keyvarchar(64)业务主键比如订单号statustinyint事务状态0未知1已提交2已回滚create_timedatetime创建时间update_timedatetime更新时间这个表的核心是status字段它必须和本地业务数据在同一事务中更新。具体操作是发送半消息拿到transactionId后在本地事务里插入一条status0的记录本地业务操作成功后把status更新为1如果业务操作失败把status更新为2。这样回查接口查询到status1就返回COMMIT_MESSAGEstatus2就返回ROLLBACK_MESSAGEstatus0就返回UNKNOW。要注意本地事务状态表的写入不要依赖外部的分布式事务框架。本地事务本身是原子的状态表与业务表同库同事务这样才能保证一致性。5. 半消息的删除与消费隔离对账确认延迟收益与脏数据风险半消息在事务提交后会被删除转入普通消息队列。这个流程是RocketMQ内部完成的业务无感知。但在实际运维中有几个细节需要关注。首先半消息的删除不是实时的。Broker虽然会及时将提交后的半消息转正但半消息在RMQ_SYS_TRANS_HALF_TOPIC中的残留可能短暂存在如果对这个Topic做消息量告警容易产生误报。建议告警时预留30秒到1分钟的容忍窗口。其次事务消息的消费隔离问题。如果多个消费组订阅同一个业务Topic半消息转正后所有消费组都会消费到这条消息。这是正常行为但要注意如果有消费组中的消费者在处理事务消息时失败重试重试次数过多消息就会进入该消费组的死信队列。这个死信队列和其他业务消息的死信队列是同一个排查时要区分消息的来源。5.1 基于事务状态表的对账机制建设对账机制是我在所有事务消息项目中都会推动的一件事。理由很简单事务消息的最终一致性是最终的但这个最终可能长达几十秒甚至几分钟。如果本地事务已经提交消息也转正了但消费端因为各种原因没处理成功消息就会一直堆积。没有一个对账机制堆积的消息很难被发现。对账可以分为两个层面一是本地事务状态表和半消息的对应关系二是消息队列中的消息和本地业务数据的对应关系。前者的目的是保证每条半消息都有明确的最终状态后者的目的是保证每条消息都被正确处理。我通常用定时任务做对账每个小时扫描一次本地事务状态表找出所有status1已提交但对应消息还没有被消费成功的记录然后查询消息消费状态确认是消费失败还是消息丢失。如果消息还在队列中等待消费结果即可如果消息已经不在队列中且消费状态未知需要手动重新发送一条补偿消息。5.2 消息轨迹数据的接入与消费补偿RocketMQ支持开启消息轨迹功能开启后可以在控制台查到消息从发送到消费的完整链路。这个功能对事务消息的对账特别有用因为事务消息的链路比普通消息更长多了一个半消息阶段。建议在事务消息的Topic上开启消息轨迹这样排查问题时可以直接看到半消息的发送时间、回查次数、转正时间、消费状态。查询消息轨迹时需要根据消息key通常是transactionId或bizKey搜索。如果没有设置消息key消息轨迹的查询效率会很低。消费补偿方面我建议把重新发送消息和更新对账状态放在同一个事务中执行避免出现消息发了但对账状态没更新的情况。补偿消息的发送使用普通消息发送接口即可不需要再走事务消息流程因为本地事务已经明确提交了。6. 半消息转正与消息丢失的边界情况SequenceId与重试补偿实录最后分享两个我在线上真实遇到过的边界情况都和半消息转正、消息丢失有关很有代表性。6.1 半消息转正后的顺序问题第一个是半消息转正后的消息顺序问题。RocketMQ不保证消息的全局顺序但可以保证同一个队列中的消息顺序。业务方如果要全局顺序一般会把消息发送到同一个队列或者消费端对同一业务主键做本地排序。事务消息对这件事的影响在于同一业务主键可能发送了多条事务消息每条消息都经历了发送半消息-提交的过程。如果业务方没有严格控制发送队列这些消息可能被发送到不同队列消费端接收到的顺序就无法保证。比如一笔订单先发送创建订单的事务消息再发送支付订单的事务消息两条消息转正后可能进入不同队列消费端可能先收到支付订单再收到创建订单。如果消费端没有做顺序处理业务逻辑就会出错。解决方案有两个一是发送消息时用消息key对队列数取模确保同一业务主键的消息进入同一队列二是消费端对同一业务主键的消息做本地排序比如根据消息中的业务时间或序号进行延迟处理。第一种方案简单有效但要注意队列数变化后比如扩缩容取模结果会变需要同步调整。6.2 消息丢失后的重试补偿实现第二个是消息丢失后的重试补偿。严格来说RocketMQ在Broker持久化消息后不会丢失消息所谓的消息丢失通常是消费端处理失败消息进入了死信队列不再被消费。某次线上排查B端反映某笔订单的状态一直没更新查了消息轨迹发现事务消息转正后B端消费失败了几次就进入了死信队列。B端的消费逻辑本身有bug抛异常后没有重试机制消息就丢了。修复方法消费端先修bug然后从死信队列拉出这条消息重新投递。但这里有个细节死信队列中的消息包含原消息的所有属性重新投递时需要保持消息key不变这样对账系统才能关联到原始事务。另外重新投递的消息不能继续使用死信队列的原始offset否则会引发消费位点异常。我实现补偿任务的流程是先扫描死信队列取出消息体、消息key、原始Topic然后调用普通消息发送接口重新发送到原始Topic。发送成功后把这条死信消息从死信队列中消费掉避免重复补偿。发送前需要确认当前分布式锁比如按消息key加锁防止多个补偿任务同时补偿同一消息。7. 事务消息实践中的最后几点提醒事务消息本身不难理解难的是在真实业务中把每个环节做扎实。最后再分享几条经验提醒都是我在维护多个生产集群后总结出来的。第一不要用事务消息做所有一致性场景。事务消息解决的是本地事务与消息发送的一致如果业务场景是跨系统的数据一致性事务消息只能保证消息发送的一致性最终一致还是得靠对账和补偿。不要指望事务消息取代分布式事务框架。第二半消息和普通消息要区分对待。监控大盘上半消息量、回查次数、死信消息量要单独建面板不要和业务消息混在一起。如果某个Topic既有事务消息又有普通消息建议拆分成两个Topic或者通过消息属性做区分否则排查问题时会非常痛苦。第三回查接口的压力测试一定不能省。本地事务接口压测很容易回查接口却经常被忽略。回查接口的并发量级可能和普通消费差不多但回查逻辑通常涉及数据库查询如果数据库性能差回查接口就会成为瓶颈。建议在压测时专门对回查接口做单独压测确认P99耗时在可接受范围内。第四定时对账是事务消息的最后一道防线。RocketMQ本身的可靠性再高也挡不住业务代码的bug。定时对账任务虽然看起来土但在关键时刻能救命。第五消费补偿要设置上限和熔断。补偿任务如果无限重发可能会把一个问题消息反复投递上百次浪费系统资源。建议补偿时设置最大重试次数比如3次或5次超过后转人工处理。希望这些内容对正在使用或计划使用RocketMQ事务消息的朋友有帮助。如果你在实际使用中遇到了其他问题欢迎在评论区留言我们一起讨论。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑