可靠消息最终一致性【本地消息表、RocketMQ 事务消息方案】
更多内容,前往IT-BLOG
一、可靠消息最终一致性事务概述
可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。 此方案是利用消息中间件完成,如下图:
事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。因此可靠消息最终一致性方案要解决以下几个问题:
【1】本地事务与消息发送的原子性问题:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。先来尝试下这种操作,先发送消息,再操作数据库:这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,据库操作失败。
1 begin transaction;
2 //1.发送MQ
3 //2.数据库操作
4 commit transation;
第二种方案,先进行数据库操作,再发送消息:这种情况下貌似没有问题,如果发送 MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但 MQ其实已经正常发送了,同样会导致不一致。
1 begin transaction;
2 //1.数据库操作
3 //2.发送MQ
4 commit transation;
【2】事务参与方接收消息的可靠性:事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
【3】消息重复消费的问题:由于步骤2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性。
二、解决方案【本地消息表方案 】
本地消息表这个方案最初是 eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。 下面以注册送积分为例来说明:下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
【交互流程如下】:【1】用户注册 :用户服务在本地事务新增用户和增加 “积分消息日志”。(用户表和消息表通过本地事务保证一致)下边是伪代码,这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原子性。
1 begin transaction;
2 //1.新增用户
3 //2.存储积分消息日志
4 commit transation;
【2】定时任务扫描日志:如何保证将消息发送给消息队列呢?经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
【3】消费消息:如何保证消费者一定能消费到消息呢?这里可以使用 MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复投递此消息。由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。
三、解决方案【RocketMQ事务消息方案 】
RocketMQ 是一个来自阿里巴巴的分布式消息中间件,于 2012 年开源,并在 2017 年正式成为 Apache 顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在 RocketMQ 之上,并且最近几年的双十一大促中,RocketMQ 都有抢眼表现。Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供了便利性支持。RocketMQ 事务消息设计主要为解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。在 RocketMQ 4.3后实现了完整的事务消息,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ内部,解决Producer 端的消息发送与本地事务执行的原子性问题。


【执行流程如下】:为方便理解我们还以注册送积分的例子来描述整个流程。Producer 即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
【1】Producer 发送事务消息:Producer (MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。本例中,Producer 发送 ”增加积分消息“ 到MQ Server。
【2】MQ Server回应消息发送成功:MQ Server接收到 Producer 发送给的消息则回应发送成功。表示 MQ已接收到消息。
【3】Producer 执行本地事务:Producer 端执行业务代码逻辑,通过本地数据库事务控制。本例中,Producer 执行添加用户操作。
【4】消息投递:若 Producer 本地事务执行成功则自动向 MQServer发送 commit消息,MQ Server接收到 Commit消息后将“增加积分消息” 状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息。若Producer 本地事务执行失败则自动向 MQServer发送 Rollback消息,MQ Server接收到 Rollback消息后将删除“增加积分消息”。MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里 ack默认自动回应,即程序执行正常则自动回应ack。
【5】事务回查:如果执行 Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他 Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态(维护本地事务状态表)即可。 RoacketMQ提供 RocketMQLocalTransactionListener接口:
1 public interface RocketMQLocalTransactionListener {
2 /**发送prepare消息成功此方法被回调,该方法用于执行本地事务
3 * @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
4 * @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
5 * @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
6 */
7 RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);
8
9 /**@param msg 通过获取transactionId来判断这条消息的本地事务执行状态
10 * @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调
11 */
12 RocketMQLocalTransactionState checkLocalTransaction(Message msg);
13 }
【6】发送事务消息:以下是 RocketMQ提供用于发送事务消息的API:
1 TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
2 producer.setNamesrvAddr("127.0.0.1:9876");
3 producer.start();
4 //设置TransactionListener实现
5 producer.setTransactionListener(transactionListener);
6 //发送事务消息
7 SendResult sendResult = producer.sendMessageInTransaction(msg, null);
四、RocketMQ实现可靠消息最终一致性事务
【业务说明】通过 RocketMQ中间件实现可靠消息最终一致性分布式事务,模拟两个账户的转账交易过程。两个账户在分别在不同的银行(张三在 bank1、李四在 bank2),bank1、bank2是两个微服务。交易过程是,张三给李四转账指定金额。 上述交易步骤,张三扣减金额与给 bank2发转账消息,两个操作必须是一个整体性的事务。
【核心代码】:程序技术架构如下:
【交互流程如下】:【1】Bank1向 MQ Server发送转账消息;
【2】Bank1执行本地事务,扣减金额;
【3】Bank2接收消息,执行本地事务,添加金额;
【数据库】:在bank1、bank2数据库中新增 de_duplication,交易记录表(去重表),用于交易幂等控制。
1 DROP TABLE IF EXISTS `de_duplication`;
2 CREATE TABLE `de_duplication` (
3 `tx_no` varchar(64) COLLATE utf8_bin NOT NULL,
4 `create_time` datetime(0) NULL DEFAULT NULL,
5 PRIMARY KEY (`tx_no`) USING BTREE
6 ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
【版本依赖】:在父工程中指定了rocketmq-spring-boot-starter的版本
1 <dependency>
2 <groupId>org.apache.rocketmq</groupId>
3 <artifactId>rocketmq-spring-boot-starter</artifactId>
4 <version>2.0.2</version>
5 </dependency>
【配置rocketMQ】:在application-local.propertis 中配置 rocketMQ nameServer地址及生产组。
1 rocketmq.producer.group = producer_bank2
2 rocketmq.name-server = 127.0.0.1:9876
【张三服务层代码】:
1 import com.alibaba.fastjson.JSONObject;
2 import org.apache.rocketmq.spring.core.RocketMQTemplate;
3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.messaging.Message;
5 import org.springframework.messaging.support.MessageBuilder;
6 import org.springframework.stereotype.Service;
7 import org.springframework.transaction.annotation.Transactional;
8
9 /**
10 * @author Administrator
11 * @version 1.0
12 **/
13 @Service
14 @Slf4j
15 public class AccountInfoServiceImpl implements AccountInfoService {
16
17 @Autowired
18 AccountInfoDao accountInfoDao;
19
20 @Autowired
21 RocketMQTemplate rocketMQTemplate;
22
23
24 //向mq发送转账消息
25 @Override
26 public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
27
28 //将accountChangeEvent转成json
29 JSONObject jsonObject =new JSONObject();
30 jsonObject.put("accountChange",accountChangeEvent);
31 String jsonString = jsonObject.toJSONString();
32 //生成message类型
33 Message<String> message = MessageBuilder.withPayload(jsonString).build();
34 //发送一条事务消息
35 /**
36 * String txProducerGroup 生产组
37 * String destination topic,
38 * Message<?> message, 消息内容
39 * Object arg 参数
40 */
41 rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1","topic_txmsg",message,null);
42
43 }
44
45 //更新账户,扣减金额
46 @Override
47 @Transactional
48 public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
49 //幂等判断,txNo是在Ctroller中生成的 UUID,全局唯一
50 if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
51 return ;
52 }
53 //扣减金额
54 accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1);
55 //添加事务日志
56 accountInfoDao.addTx(accountChangeEvent.getTxNo());
57 if(accountChangeEvent.getAmount() == 3){
58 throw new RuntimeException("人为制造异常");
59 }
60 }
61 }
【张三RocketMQLocalTransactionListener】:编写 RocketMQLocalTransactionListener接口实现类,实现执行本地事务和事务回查两个方法。
1 import com.alibaba.fastjson.JSONObject;
2 import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
3 import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
4 import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
5 import org.springframework.messaging.Message;
6 import org.springframework.transaction.annotation.Transactional;
7
8 /**
9 * @author Administrator
10 * @version 1.0
11 **/
12 @Component
13 @Slf4j
14 //生产者组与发送消息时定义组相同
15 @RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
16 public class ProducerTxmsgListener implements RocketMQLocalTransactionListener {
17
18 @Autowired
19 AccountInfoService accountInfoService;
20
21 @Autowired
22 AccountInfoDao accountInfoDao;
23
24 //事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
25 @Override
26 @Transactional
27 public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
28
29 try {
30 //解析message,转成AccountChangeEvent
31 String messageString = new String((byte[]) message.getPayload());
32 JSONObject jsonObject = JSONObject.parseObject(messageString);
33 String accountChangeString = jsonObject.getString("accountChange");
34 //将accountChange(json)转成AccountChangeEvent
35 AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
36 //执行本地事务,扣减金额
37 accountInfoService.doUpdateAccountBalance(accountChangeEvent);
38 //当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
39 return RocketMQLocalTransactionState.COMMIT;
40 } catch (Exception e) {
41 e.printStackTrace();
42 return RocketMQLocalTransactionState.ROLLBACK;
43 }
46 }
47
48 //事务状态回查,查询是否扣减金额
49 @Override
50 public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
51 //解析message,转成AccountChangeEvent
52 String messageString = new String((byte[]) message.getPayload());
53 JSONObject jsonObject = JSONObject.parseObject(messageString);
54 String accountChangeString = jsonObject.getString("accountChange");
55 //将accountChange(json)转成AccountChangeEvent
56 AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
57 //事务id
58 String txNo = accountChangeEvent.getTxNo();
59 int existTx = accountInfoDao.isExistTx(txNo);
60 if(existTx>0){
61 return RocketMQLocalTransactionState.COMMIT;
62 }else{
63 return RocketMQLocalTransactionState.UNKNOWN;
64 }
65 }
66 }
【李四服务层代码】:
1 import org.springframework.stereotype.Service;
2 import org.springframework.transaction.annotation.Transactional;
3
4 /**
5 * @author Administrator
6 * @version 1.0
7 **/
8 @Service
9 @Slf4j
10 public class AccountInfoServiceImpl implements AccountInfoService {
11
12 @Autowired
13 AccountInfoDao accountInfoDao;
14
15 //更新账户,增加金额
16 @Override
17 @Transactional
18 public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
19 log.info("bank2更新本地账号,账号:{},金额:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
20 if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo())>0){
21 return ;
22 }
23 //增加金额
24 accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount());
25 //添加事务记录,用于幂等
26 accountInfoDao.addTx(accountChangeEvent.getTxNo());
27 if(accountChangeEvent.getAmount() == 4){
28 throw new RuntimeException("人为制造异常");
29 }
30 }
31 }
【MQ监听类】:通过实现 RocketMQListener接口监听目标 Topic
1 import com.alibaba.fastjson.JSONObject;
2 import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
3 import org.apache.rocketmq.spring.core.RocketMQListener;
4
5 /**
6 * @author Administrator
7 * @version 1.0
8 **/
9 @Component
10 @Slf4j
11 @RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2",topic = "topic_txmsg")
12 public class TxmsgConsumer implements RocketMQListener<String> {
13
14 @Autowired
15 AccountInfoService accountInfoService;
16
17 //接收消息
18 @Override
19 public void onMessage(String message) {
20 log.info("开始消费消息:{}",message);
21 //解析消息
22 JSONObject jsonObject = JSONObject.parseObject(message);
23 String accountChangeString = jsonObject.getString("accountChange");
24 //转成AccountChangeEvent
25 AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
26 //设置账号为李四的
27 accountChangeEvent.setAccountNo("2");
28 //更新本地账户,增加金额
29 accountInfoService.addAccountInfoBalance(accountChangeEvent);
31 }
32 }
五、总结
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,本案例使用了 RocketMQ作为消息中间件,RocketMQ主要解决了两个功能:
【1】本地事务与消息发送的原子性问题;
【2】事务参与方接收消息的可靠性;
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
可靠消息最终一致性【本地消息表、RocketMQ 事务消息方案】的更多相关文章
- RocketMQ 事务消息
RocketMQ 事务消息在实现上充分利用了 RocketMQ 本身机制,在实现零依赖的基础上,同样实现了高性能.可扩展.全异步等一系列特性. 在具体实现上,RocketMQ 通过使用 Half To ...
- .Net Core with 微服务 - 分布式事务 - 可靠消息最终一致性
前面我们讲了分布式事务的2PC.3PC , TCC 的原理.这些事务其实都在尽力的模拟数据库的事务,我们可以简单的认为他们是一个同步行的事务.特别是 2PC,3PC 他们完全利用数据库的事务能力,在一 ...
- rocketmq事务消息
rocketmq事务消息 参考: https://blog.csdn.net/u011686226/article/details/78106215 https://yq.aliyun.com/art ...
- 搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务
搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务 初步认识RocketMQ的核心模块 rocketmq模块 rocketmq-broker:接受生产者发来的消息并存储(通过调用rocke ...
- rocketmq事务消息入门介绍
说明 周五的时候发了篇:Rocketmq4.3支持事务啦!!!,趁着周末的时候把相关内容看了下,下面的主要内容就是关于RocketMQ事务相关内容介绍了. 说明: 今天这篇仅仅是入门介绍,并没有涉及到 ...
- RocketMQ之四:RocketMq事务消息
事务消息 通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,从而保证了数据的最终一致性. 发送端执行如下几步: 发送prepare消息,该消息对Consumer不可见 执行本地事务(如 ...
- 关于 RocketMQ 事务消息的正确打开方式 → 你学废了吗
开心一刻 昨晚和一哥们一起吃夜宵,点了几瓶啤酒 不一会天空下起了小雨,哥们突然道:糟了 我:怎么了 哥们:外面下雨了,我老婆还在等着我去接她 他给了自己一巴掌,说道:真他妈不是个东西 我心想:哥们真是 ...
- RocketMQ 事务消息示例分析
@ 目录 1 示例模式 2 安装与配置 RocketMQ 3 运行服务 3.1 启动 NameServer 3.2 启动 broker 4 生产者 4.1 事务监听器 4.2 事务消息生产者 5 消费 ...
- RocketMQ源码 — 十一、 RocketMQ事务消息
分布式事务是一个复杂的问题,rmq实现了事务的最终一致性,rmq保证本地事务成功消息一定会发送成功并被成功消费,如果本地事务失败了,消息不会被发送. rmq事务消息的实现过程为: producer发送 ...
- RocketMQ事务消息学习及刨坑过程
一.背景 MQ组件是系统架构里必不可少的一门利器,设计层面可以降低系统耦合度,高并发场景又可以起到削峰填谷的作用,从单体应用到集群部署方案,再到现在的微服务架构,MQ凭借其优秀的性能和高可靠性,得到了 ...
随机推荐
- 使用python+poco+夜神模拟器进行自动化测试。
https://blog.csdn.net/saint_228/article/details/84889017 网易最近出的一款自动化UI测试工具:Airtest 挺火的,还受到谷歌的推荐.我试着用 ...
- XSS跨站脚本攻击(Cross Site Scripting)
XSS是跨站脚本攻击(Cross Site Scripting),不写为CSS是为了避免和层叠样式表(Cascading Style Sheets)的缩写混淆,所以将跨站脚本攻击写为XSS. 攻击者可 ...
- 【批量下载】url——wget,给定网址,批量下载文件
写在前面:最近在学习批量下载sci文章并传入noteexpress,从网上学来了一些方法,其中有一步需要使用url将sci-hub上面的文章批量下载下来. 学习网址:[研究生]快速批量下载 Web o ...
- linux安装等
软碟通刻入某版本linux 推荐方法 2) 1) U盘启动 修改vmlinuz initrd=initrd.img linux dd quiet 查看U盘盘符 重启修改hd:/dev/sd** qui ...
- PHP Redis - Set(集合)
Redis 的 set 无序集合,与 list 类似,特殊之处在于 set 可以自动排重,不会出现重复数据 集合中最大的成员数为 232-1 (4294967295, 每个集合可存储40多亿个成员). ...
- 【编程】Python3 正则表达式使用笔记
前言 Python 从1.5版本开始使用re模块来处理正则表达式.我们可以使用"re模块"或"re.compile方法"来创建正则表达式对象(re.RegexO ...
- PLC入门笔记8
梯形图基础电路 起保停电路 多点起保停电路 互锁控制电路 周期闪烁电路 这应该是等价的!! 定时器的接力电路 同 延时接通,延时断开电路 同 保持信号变脉冲信号电路 定时器TON 接通延时变断开延时电 ...
- 需要改动node_modules,并且别人也可以同步,插件 patch-package
patch-package 转自:https://www.cnblogs.com/lovewhatIlove/p/15724812.html 1.简介:有个功能需要修改node_modules里面的代 ...
- PHP实现JWT登录鉴权
一.什么是JWT 1.简介 JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准. 简单的说,JWT就是一种Token的编码算法,服务器端负责根据一个 ...
- AcWing 66. 两个链表的第一个公共结点 (2012算法题)
题目: 输入两个链表,找出它们的第一个公共结点. 当不存在公共节点时,返回空节点. 数据范围 链表长度 [1,2000]. 保证两个链表不完全相同,即两链表的头结点不相同. 样例 给出两个链表如下所 ...