关于 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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
-- 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版本 ...
随机推荐
- windows环境下配置Kafaka
一.安装Zookeeper(Kafaka依赖于zookeeper进行服务注册和管理) 1. 1 下载zookeeper:http://mirror.bit.edu.cn/apache/zookee ...
- FastAPI(45)- JSONResponse
背景 创建 FastAPI 路径操作函数时,通常可以从中返回任何数据:字典.列表.Pydantic 模型.数据库模型等 默认情况下,FastAPI 会使用 jsonable_encoder 自动将该返 ...
- bash是什么?
bash shell就是一个bash程序 --解释器,启动器 --解释器: 用户交互输入 如vim 文本文件输入 脚本本质: !/bin/bash !/usr/bin/python 读取方 ...
- maven配置下载包 解决SunCertPathBuilderException:unable to find valid certification path to requested target
解决 『SunCertPathBuilderException:unable to find valid certification path to requested target』 问题 ★ ...
- exe图标消失的解决方案
步骤 win + r组合键打开运行窗口 输入cmd,回车 在终端窗口右键粘贴即可 taskkill /im explorer.exe /f cd /d %userprofile%\appdata\lo ...
- Golang通脉之基础入门
为什么要学 Go 性能优越感:Go 极其地快,其性能与 Java 或 C++相似.在使用中,Go 一般比 Python 要快 30 倍: 序列化/去序列化.排序和聚合中表现优异: 开发者效率较高:多种 ...
- javascript-jquery的基本方法
1.去除字符串中两端的空格$.trim(str) var str1=" 123 " $.trim(str1);//123 2.遍历对象的数据并进行操作$.each(obj,func ...
- .Net Core中使用ElasticSearch(一)
一.安装配置 在官网下载Es,注意版本号,不同大版本号之间差异很大.我安装的是7.14.0版本 1.1 安装成服务 cmd 进入bin目录下执行 elasticsearch-service.bat i ...
- 基于Apache Zookeeper手写实现动态配置中心(纯代码实践)
相信大家都知道,每个项目中会有一些配置信息放在一个独立的properties文件中,比如application.properties.这个文件中会放一些常量的配置,比如数据库连接信息.线程池大小.限流 ...
- UltraSoft - Beta - Scrum Meeting 11
Date: May 27th, 2020. Scrum 情况汇报 进度情况 组员 负责 今日进度 q2l PM.后端 记录会议 Liuzh 前端 增加了对重复日程的支持 Kkkk 前端 测试验证前后端 ...