关于 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版本 ...
随机推荐
- AT3950-[AGC022E]Median Replace【贪心,dp】
正题 题目链接:https://www.luogu.com.cn/problem/AT3950 题目大意 一个包含\(?,0,1\)的长度为奇数的序列,把\(?\)替换为\(0/1\).每次可以选择三 ...
- SVN服务器的环境搭建(图文并茂)
SVN服务器的搭建与使用 本章读完,就可以创建代码版本库,传送门 https://www.cnblogs.com/zzpython/p/13348009.html TortoiseSVN 是 Subv ...
- 巧用优先队列:重载运算符在STL优先队列中的应用
前言 写优先队列优化dijkstra的时候,需要放进优先队列的常常有数值和编号两类,以下介绍让编号捆绑数值放入优先队列的几种方法. 由于过程比较简单,记住代码即可,下面不再讲解,只附上代码,请读者自行 ...
- python-docx处理Word必备工具
我的理解 为什么会用到python-docx,因为近段时间下载了大量网文,但格式都是html的,我个人习惯使用word处理文字,于是就想法设法把html文档转换为word,首先要考虑的问题就是从h ...
- opencv中的exp32f函数
exp32f opencv的exp函数和cmath的exp函数在精度上存在一定差异,通过查找源码,发现了这么一段实现.代码如下: 点击查看代码 #define EXPTAB_SCALE 6 #defi ...
- 舌头算法的C++实现
观察生活,我们不难发现,吃饭的时候,有时候左边的东西会到右边来,这是为什么呢?就是舌头的作用了. 下面的代码将模拟舌头的运动: #include <iostream> #include & ...
- java实现责任链模式的小demo
//一个请假请求 public class LeaveRequest { private int leaveDays; private String name; public void leave() ...
- Spark RDD编程(博客索引,日常更新)
本篇主要是记录自己在中解决RDD编程性能问题中查阅的论文博客,为我认为写的不错的建立索引方便查阅,我的总结会另立他篇 1)通过分区(Partitioning)提高spark性能https://blog ...
- Poetry(2)Poetry的基本使用方式
Poetry的基本使用 准备工作 如果你是在一个已有的项目里使用Poetry,你只需要执行 poetry init 命令来创建一个 pyproject.toml 文件: poetry init 可看到 ...
- 什么,你还使用 webpack?别人都在用 vite 搭建项目了
一.vite 到底是干嘛的? vite 实际上就是一个面向现代浏览器,基于 ES module 实现了一个更轻快的项目构建打包工具. vite 是法语中轻快的意思. vite 的特点: 1.轻快的冷服 ...