关于 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版本 ...
随机推荐
- CAS邮箱的Express配置
Configuration for all clients: http://help.cstnet.cn/changjianwenti/youjianshoufa/kehuduan.htm Confi ...
- Mysql explain中key_len的作用及计算规则
key_len表示索引使用的字节数,根据这个值可以判断索引的使用情况,特别是在组合索引的时候,判断该索引有多少部分被使用到非常重要. 在计算key_len时,下面是一些需要考虑的点: 索引字段的附加信 ...
- video 适配通屏展示、针对不同分辨率 禁止变形处理
CSS object-fit 属性 object-fit: fill|contain|cover|scale-down|none|initial|inherit; 样式上 video{ height: ...
- 从零入门 Serverless | 函数计算的开发与配置
导读:在本篇文章中,"基本概念"部分主要对函数计算最核心的概念进行详细介绍,包括服务.函数.触发器.版本.别名以及相关的配置:"开发流程"部分介绍了基于函数计算 ...
- mysql update语句的执行流程是怎样的
update更新语句流程是怎么样的 update更新语句基本流程也会查询select流程一样,都会走一遍. update涉及更新数据,会对行加dml写锁,这个DML读锁是互斥的.其他dml写锁需要等待 ...
- Win10开启剪贴板
点击任务栏下方右侧的会话窗口 点击所有设置 在搜索栏中输入剪贴板,点击进入剪贴板设置 开启剪贴板历史记录 按下组合键win + v即可呼出剪贴板
- Java(7)流程控制语句中的for、while、do while循环
作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201543.html 博客主页:https://www.cnblogs.com/testero ...
- python 工具箱
strip() 方法可以从字符串去除不想要的空白符. print() BIF的file参数控制将数据发送/保存到哪里. finally组总会执行,而不论try/except语句中出现什么异常. 会向e ...
- javascript-jquery介绍
jquery优势 1.轻量级 2.强大的选择器 3.出色的DOM封装 4.可靠的事件处理机制 5.完善的Ajax 6.不污染顶级变量 7.出色的浏览器兼容 8.链式操作方式 9.隐式迭代 10.行为层 ...
- 【Azure 应用服务】App Service For Linux 如何在 Web 应用实例上住抓取网络日志
问题描述 在App Service For Windows的环境中,我们可以通过ArmClient 工具发送POST请求在Web应用的实例中抓取网络日志,但是在App Service For Linu ...