关于 RocketMQ 事务消息的正确打开方式 → 你学废了吗
开心一刻
昨晚和一哥们一起吃夜宵,点了几瓶啤酒
不一会天空下起了小雨,哥们突然道:糟了
我:怎么了
哥们:外面下雨了,我老婆还在等着我去接她
他给了自己一巴掌,说道:真他妈不是个东西
我心想:哥们真是个好丈夫
很快他补充道:喝酒怎么能分心呢
我一口啤酒直接笑喷而出

知识回顾
本文不讲什么是 RocketMQ ,不讲它的实现原理,只想和大家探讨下它的事务消息的正确使用方式
再探讨之前,先带大家回顾下知识点
事务消息的设计原理
RocketMQ 在 4.3.0 版中已经支持分布式事务消息,采用 2PC 的思想实现事务消息提交,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示

什么,英文看不懂?贴心的我早已想到,中文版的也有

其中有两个点:半事务、回查事务状态,值得我们重点回顾
Half 消息
何谓 half 消息?
消息发送方把消息发送到 MQ 服务,但是此消息的状态被标记为不能投递,处于这种状态下的消息称为 half 消息;消费方不能消费 half 消息
发送方对 half 消息二次确认后,也就是 Commit 之后,消费方才可以消费到;如果是 Rollback,该消息则会被删除,永远不会被消费到
事务状态回查
如果在 RocketMQ 事务消息的二阶段过程中失败了,例如在做 Commit 操作时(上图中的第 4 步),出现网络问题导致 Commit 失败,那么需要通过一定的策略使这条消息最终被 Commit
RocketMQ 采用了一种补偿机制,称为“回查”。Broker 端对未确定状态的消息发起回查,将消息发送到对应的 Producer 端(同一个 Group 的 Producer),由 Producer 根据消息来检查本地事务的状态,进而执行 Commit 或者 Rollback
值得注意的是,RocketMQ 并不会无休止的的信息事务状态回查,默认回查 15 次,如果 15 次回查还是无法得知事务状态,RocketMQ 默认回滚该消息
更多细节请查看:事务消息
实战示例
理论知识理解之后,就需要我们进行实操与分析了
需求背景
假设我们有两个服务:订单服务、积分服务,当用户成功下单之后,需要给用户加相应的积分
实现方式有很多种,你知道哪些?
假设我们用 RocketMQ 事务消息来保证最终一致性,我们又该如何实现?
环境准备
RocketMQ:4.8.0
rocketmq-client:4.9.2
Spring Boot:2.1.0.RELEASE
MySQL:5.7.29
MyBatis Plus:3.4.2
建表 SQL

-- order
CREATE TABLE `order`.`t_order` (
`order_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`order_no` char(20) NOT NULL COMMENT '订单号',
`user_id` bigint(32) NOT NULL COMMENT '用户id',
`order_amount` decimal(16,2) NOT NULL,
`note` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 不一定非要存half消息的事务id,实现方式有很多,甚至可以不用这张表,直接通过 t_order 新增字段来实现
CREATE TABLE `order`.`t_order_transaction_log` (
`transaction_id` varchar(32) NOT NULL COMMENT '主键(half 消息的事务id)',
`order_id` bigint(20) NOT NULL COMMENT '订单主键',
`note` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`transaction_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- points
CREATE TABLE `points`.`t_point` (
`point_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_no` char(20) NOT NULL COMMENT '订单号',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`point_num` decimal(16,2) NOT NULL COMMENT '积分数量',
`note` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`point_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
项目地址:spring-boot-rocketmq-order,spring-boot-rocketmq-points
后续只会对关键代码进行讲解,所以建议大家把代码 down 下来看看,保证有个基本的印象
回到标题,楼主为什么会强调:正确的打开方式
你猜对了,RocketMQ 事务消息的使用方式有很多种,楼主就结合工作项目中的使用方式,来和大家一起讨论下,哪些方式是正确的,哪些方式是不正确的(以及不正确的原因)
结合 Half 消息发送的时机,大致可分为三种:

根据 half 消息的位置,我们暂且将这三种方式命名为:half 消息后置、half 消息中置、half 消息前置
我们逐个来讨论使用是否正确
half 消息后置
这种方式有没有觉得似曾相识?与发普通消息是不是很类似? 本地业务执行完之后,发普通消息给积分中心,是不是熟悉的味道?
但还是有区别的,至少有回查机制,我们结合伪代码具体看看


我们来分析下各种异常情况,看看这种方式是否有问题
1、订单数据或订单事务日志落库异常,事务回滚,half 消息不会发送,没问题
2、half 消息发送异常,事务会回滚,没问题
3、half 消息发送未发生异常,但返回的不是 SEND_OK 状态,代码抛出了异常,事务回滚,没问题
思考:如果我们不关注 half 消息发送的结果,像这样

最终,消息会推送给积分服务吗?
虽然看起来怪怪的,但又挑不出毛病
half 消息中置
我们直接看伪代码


我们来分析下各种异常情况,看看这种方式是否有问题
1、订单数据落库异常,事务回滚,half 消息不会发送,没问题
2、half 消息发送异常,事务会回滚,没问题
3、half 消息发送未发生异常,但返回的不是 SEND_OK 状态,代码抛出异常,事务会回滚,没问题
思考:与之前的思考问题一样,如果我们不关注 half 消息发送的结果,最终消息会推送给积分服务吗?
只有发送 half 消息成功,并且发送状态为 SEND_OK ,才会执行 executeLocalTransaction ,向 t_order_transaction_log 表写入事务日志
那么即使 Broker 回查事务状态,它得到的结果始终是 UNKNOW ,最终 half 消息会被回滚,积分服务收不到消息
导致的问题就是:用户下单成功,但却没有增加积分
可见关注 half 消息发送结果的重要性
4、half 消息发送成功,且返回的是 SEND_OK 状态,但 executeLocalTransaction 执行异常了,会是什么结果?
代码很明显,我们进行了 catch ,异常不会向上抛,订单落库还是成功的,只是订单事务日志落库失败了
返回 ROLLBACK_MESSAGE ,half 消息会回滚,积分服务收不到消息
那么同样的问题又出现了:用户下单成功,但却没有增加积分
如果我们不 catch ,像这样

理论上来讲,异常往上抛,订单数据会回滚, Broker 回查事务状态,一直返回 UNKNOW ,最终积分服务收不到消息
理论上来讲没问题,但事实呢? 我们来实践一下

哦豁,竟然没有打印异常日志,也就说异常被 catch 没有往外抛,订单数据也落库了
那么又会出现同样的问题:用户下单成功,但却没有增加积分
至于谁把异常 catch 了没往外抛,相信大家都能想到,这算是 rocketmq-client 的一个 bug ;源码稍后再跟,我们先看完前置
half 消息前置
直接上伪代码



我们来分析下各种异常情况,看看这种方式是否有问题
1、half 消息发送异常,本地事务不会执行,没问题
2、half 消息发送未发生异常,但返回的不是 SEND_OK 状态,代码抛出异常,本地事务不会执行,没问题
思考:与之前的思考问题一样,如果我们不关注 half 消息发送的结果,会是什么结果?
只有 half 消息发送成功,且返回状态是 SEND_OK 才会执行 executeLocalTransaction
即使 Broker 回查事务状态,得到的结果始终是 UNKNOW ,最终 half 消息会被回滚,积分服务收不到消息
订单服务与积分服务都没有落库成功,也就说是没问题的
3、half 消息发送成功,且返回的状态是 SEND_OK ,但 executeLocalTransaction 执行异常了,会是什么结果
也就是 save 方法执行异常了,我们来实践下

异常还是被 catch 了没往外抛,但是订单数据却回滚了,就结果而言是没问题的
half 消息发送成功了,但是 Broker 一直未收到本地事务的确认消息, Broker 会回查,得到的结果始终是 UNKNOW ,最终 half 消息会被回滚,积分服务收不到消息
订单数据回滚了,积分服务未收到消息,那么此种情况是没问题的
看起来挺顺眼,异常情况下也没什么问题
rocketmq-client 的 bug
需要弄清楚的问题有两个:
1、half 消息中置, executeLocalTransaction 的异常为什么没有抛出来
2、half 消息前置, 异常同样没有抛出来,为什么订单数据却回滚了
先看第一个问题,我们来跟下源码

rocketmq-client 捕获了异常,但并未向外抛
其实 RocketMQ 是有打印日志的,只是楼主的日志配置的不对,导致控制台未打印出来
对于第 1 个问题,相信大家已经清楚了
关于第 2 个问题,我就不具体分析了,我给个提示,从事务 AOP 的控制范围与异常抛出点来考虑,如下图


最终一致性
前面讲了那么多,都是讲的订单服务,总结起来就是:事务消息(而非 half 消息)发送成功,那么本地事务一定是执行成功的
保证的是事务消息的发送与订单服务的强一致
如果积分服务消费异常呢?
那对不起,RocketMQ 事务消息处理不了这种情况,回滚不了订单服务的数据,只能通过补偿机制(比如人工修复)修复积分服务的数据
总结
1、三种方式的抉择
half 消息中置,问题比较多,不推荐
half 消息后置,看起来挺别扭的(难道只是楼主这么觉得?),倒是没什么问题
half 消息前置,符合 RocketMQ 事务消息的设计原理,推荐采用此种方式
2、一定要关注 half 消息发送的结果,不抛异常不代表一定成功了,必要时需要根据 half 消息发送的结果做后续逻辑处理
3、最终一致性
RocketMQ 考虑的是数据最终一致性,上游服务提交之后,下游服务最终只能成功,做不到回滚上游服务的数据
参考
关于 RocketMQ 事务消息的正确打开方式 → 你学废了吗的更多相关文章
- RocketMQ事务消息学习及刨坑过程
一.背景 MQ组件是系统架构里必不可少的一门利器,设计层面可以降低系统耦合度,高并发场景又可以起到削峰填谷的作用,从单体应用到集群部署方案,再到现在的微服务架构,MQ凭借其优秀的性能和高可靠性,得到了 ...
- rocketmq事务消息
rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- RocketMQ事务消息实现分析
这周RocketMQ发布了4.3.0版本,New Feature中最受关注的一点就是支持了事务消息: 今天花了点时间看了下具体的实现内容,下面是简单的总结. RocketMQ事务消息概要 通过冯嘉发布 ...
- C#语法——泛型的多种应用 C#语法——await与async的正确打开方式 C#线程安全使用(五) C#语法——元组类型 好好耕耘 redis和memcached的区别
C#语法——泛型的多种应用 本篇文章主要介绍泛型的应用. 泛型是.NET Framework 2.0 版类库就已经提供的语法,主要用于提高代码的可重用性.类型安全性和效率. 泛型的定义 下面定义了 ...
- 任务队列和异步接口的正确打开方式(.NET Core版本)
任务队列和异步接口的正确打开方式 什么是异步接口? Asynchronous Operations Certain types of operations might require processi ...
- 搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务
搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务 初步认识RocketMQ的核心模块 rocketmq模块 rocketmq-broker:接受生产者发来的消息并存储(通过调用rocke ...
- RocketMQ之四:RocketMq事务消息
事务消息 通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,从而保证了数据的最终一致性. 发送端执行如下几步: 发送prepare消息,该消息对Consumer不可见 执行本地事务(如 ...
- RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想
摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...
随机推荐
- 【Azure Developer】Azure Automation 自动化账号生成的时候怎么生成连接 与证书 (Connection & Certificate)
Azure Automation :The Azure Automation service provides a highly reliable and scalable workflow exec ...
- 从零入门 Serverless | SAE 的远程调试和云端联调
作者 | 弈川 阿里巴巴云原生团队 导读:本节课程包含三部分内容,前两个部分简单介绍远程调试以及端云联调的原理,最后在 Serverless 应用引擎中进行实际演示. 经过之前课程的学习,相信大家对于 ...
- redis学习笔记-02 list列表类型命令
一.lpush key value1 value2 value3 value4(命令将一个或多个值插入到列表头部. 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作) lpush k1 ...
- 题解「BZOJ4310」跳蚤
题目传送门 Description 现在有一个长度为 \(n\) 的字符串,将其划分为 \(k\) 段,使得这 \(k\) 段每一段的字典序最大子串中字典序最大的字符串字典序尽量小.求出这个字符串. ...
- 重学c#系列——字典(十一)
前言 重学c#系列继续更新,简单看一下字典的源码. 看源码主要是解释一下江湖中的两个传言: 字典foreach 顺序是字典添加的顺序 字典删除元素后,字典顺序将会改变 正文 那么就从实例化开始看起,这 ...
- 6. 站在巨人的肩膀学习Java Filter型内存马
本文站在巨人的肩膀学习Java Filter型内存马,文章里面的链接以及图片引用于下面文章,参考文章: <Tomcat 内存马学习(一):Filter型> <tomcat无文件内存w ...
- 实用小工具:screen
实用小工具:screen 首先,吹爆screen screen,实现了不间断的会话服务,通过SSH连接至远程服务器,当使用了screen开启的会话,不会因为你断开SSH而中断在远程服务器上运行的命令. ...
- UltraSoft - Alpha - Scrum Meeting 6
Date: Apr 21th, 2020. Scrum 情况汇报 进度情况 组员 负责 昨日进度 后两日任务 CookieLau PM 验证了课程中心获取课程资源和作业的爬虫方式 细化前后端交互中的难 ...
- RocketMQ源码详解 | Producer篇 · 其一:Start,然后 Send 一条消息
概述 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); ...
- 2021.8.15考试总结[NOIP模拟40]
T1 送花 线段树.枚举右端点,线段树记录左端点对应的值. 每次对当前颜色上上次出现的位置到上次出现的位置区间减,上次出现的位置到当前位置区间加. $code:$ 1 #include<bits ...