开心一刻

  昨晚和一哥们一起吃夜宵,点了几瓶啤酒

  不一会天空下起了小雨,哥们突然道:糟了

  我:怎么了

  哥们:外面下雨了,我老婆还在等着我去接她

  他给了自己一巴掌,说道:真他妈不是个东西

  我心想:哥们真是个好丈夫

  很快他补充道:喝酒怎么能分心呢

  我一口啤酒直接笑喷而出

知识回顾

  本文不讲什么是 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-orderspring-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 事务消息的正确打开方式 → 你学废了吗的更多相关文章

  1. RocketMQ事务消息学习及刨坑过程

    一.背景 MQ组件是系统架构里必不可少的一门利器,设计层面可以降低系统耦合度,高并发场景又可以起到削峰填谷的作用,从单体应用到集群部署方案,再到现在的微服务架构,MQ凭借其优秀的性能和高可靠性,得到了 ...

  2. rocketmq事务消息

    rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...

  3. RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)

    在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...

  4. RocketMQ事务消息实现分析

    这周RocketMQ发布了4.3.0版本,New Feature中最受关注的一点就是支持了事务消息: 今天花了点时间看了下具体的实现内容,下面是简单的总结. RocketMQ事务消息概要 通过冯嘉发布 ...

  5. C#语法——泛型的多种应用 C#语法——await与async的正确打开方式 C#线程安全使用(五) C#语法——元组类型 好好耕耘 redis和memcached的区别

    C#语法——泛型的多种应用   本篇文章主要介绍泛型的应用. 泛型是.NET Framework 2.0 版类库就已经提供的语法,主要用于提高代码的可重用性.类型安全性和效率. 泛型的定义 下面定义了 ...

  6. 任务队列和异步接口的正确打开方式(.NET Core版本)

    任务队列和异步接口的正确打开方式 什么是异步接口? Asynchronous Operations Certain types of operations might require processi ...

  7. 搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务

    搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务 初步认识RocketMQ的核心模块 rocketmq模块 rocketmq-broker:接受生产者发来的消息并存储(通过调用rocke ...

  8. RocketMQ之四:RocketMq事务消息

    事务消息 通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,从而保证了数据的最终一致性. 发送端执行如下几步: 发送prepare消息,该消息对Consumer不可见 执行本地事务(如 ...

  9. RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想

    摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...

随机推荐

  1. CAS邮箱的Express配置

    Configuration for all clients: http://help.cstnet.cn/changjianwenti/youjianshoufa/kehuduan.htm Confi ...

  2. Mysql explain中key_len的作用及计算规则

    key_len表示索引使用的字节数,根据这个值可以判断索引的使用情况,特别是在组合索引的时候,判断该索引有多少部分被使用到非常重要. 在计算key_len时,下面是一些需要考虑的点: 索引字段的附加信 ...

  3. video 适配通屏展示、针对不同分辨率 禁止变形处理

    CSS object-fit 属性 object-fit: fill|contain|cover|scale-down|none|initial|inherit; 样式上 video{ height: ...

  4. 从零入门 Serverless | 函数计算的开发与配置

    导读:在本篇文章中,"基本概念"部分主要对函数计算最核心的概念进行详细介绍,包括服务.函数.触发器.版本.别名以及相关的配置:"开发流程"部分介绍了基于函数计算 ...

  5. mysql update语句的执行流程是怎样的

    update更新语句流程是怎么样的 update更新语句基本流程也会查询select流程一样,都会走一遍. update涉及更新数据,会对行加dml写锁,这个DML读锁是互斥的.其他dml写锁需要等待 ...

  6. Win10开启剪贴板

    点击任务栏下方右侧的会话窗口 点击所有设置 在搜索栏中输入剪贴板,点击进入剪贴板设置 开启剪贴板历史记录 按下组合键win + v即可呼出剪贴板

  7. Java(7)流程控制语句中的for、while、do while循环

    作者:季沐测试笔记 原文地址:https://www.cnblogs.com/testero/p/15201543.html 博客主页:https://www.cnblogs.com/testero ...

  8. python 工具箱

    strip() 方法可以从字符串去除不想要的空白符. print() BIF的file参数控制将数据发送/保存到哪里. finally组总会执行,而不论try/except语句中出现什么异常. 会向e ...

  9. javascript-jquery介绍

    jquery优势 1.轻量级 2.强大的选择器 3.出色的DOM封装 4.可靠的事件处理机制 5.完善的Ajax 6.不污染顶级变量 7.出色的浏览器兼容 8.链式操作方式 9.隐式迭代 10.行为层 ...

  10. 【Azure 应用服务】App Service For Linux 如何在 Web 应用实例上住抓取网络日志

    问题描述 在App Service For Windows的环境中,我们可以通过ArmClient 工具发送POST请求在Web应用的实例中抓取网络日志,但是在App Service For Linu ...