关于 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版本 ...
随机推荐
- Spring Cloud Gateway 雪崩了,我 TM 人傻了
本系列是 我TM人傻了 系列第六期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 获取异 ...
- Hibernate 的 <= 出现问题
问题模拟 select new map( e.name as name , e.salary as salary) from Emplpyee e where e.salary <= :sala ...
- php flush() 页面缓冲及时输出 每隔一秒输出页面输出
<?php //方案一 ob_end_clean(); echo str_pad('', 1024); // 设置足够大,大过php.ini的output_buffering设置值 for ($ ...
- PC Register简介
PC Regiter介绍: JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载 ...
- 洛谷2494 [SDOI2011]保密 (分数规划+最小割)
自闭一早上 分数规划竟然还能被卡精度 首先假设我们已经知道了到每个出入口的时间(代价) 那我们应该怎么算最小的和呢? 一个比较巧妙的想法是,由于题目规定的是二分图. 我们不妨通过最小割的形式. 表示这 ...
- HTML5元素背景知识
目录 HTML5元素背景知识 语义与呈现分离 元素选用原则 少亦可为多 别误用元素 具体为佳,一以贯之 对用户不要想当然 元素说明体例 ol元素 元素速览 文档和元数据元素 文档和元数据元素 文本元素 ...
- 网络协议之:加密传输中的NPN和ALPN
目录 简介 SSL/TLS协议 NPN和ALPN 交互的例子 总结 简介 自从HTTP从1.1升级到了2,一切都变得不同了.虽然HTTP2没有强制说必须使用加密协议进行传输,但是业界的标准包括各大流行 ...
- javascript-jquery-更改jquery对象
在许多情况下,jquery代码所做的事情变成了:生成jquery对象A,操作对jquery象A:更改为jquery对象B,操作jquery对象B:更改为jqueryC,操作jquery对象C..... ...
- 【Spring】IoC容器 - Spring Bean作用域Scope(含SpringCloud中的RefreshScope )
前言 上一章学习了[依赖来源],本章主要讨论SpringBean的作用域,我们这里讨论的Bean的作用域,很大程度都是默认只讨论依赖来源为[Spring BeanDefinition]的作用域,因为在 ...
- 【Java虚拟机1】Java字节码文件格式入门
第一次学习看字节码文件,这个对工作没什么用,但是会提升内功. 首先介绍两个IDEA插件以及使用: BinEd:以16进制格式查看class文件 使用方法:右键class文件,点击Open as bin ...