分布式消息队列RocketMQ--事务消息--解决分布式事务
说到分布式事务,就会谈到那个经典的”账号转账”问题:2个账号,分布处于2个不同的DB,或者说2个不同的子系统里面,A要扣钱,B要加钱,如何保证原子性?
一般的思路都是通过消息中间件来实现“最终一致性”:A系统扣钱,然后发条消息给中间件,B系统接收此消息,进行加钱。
但这里面有个问题:A是先update DB,后发送消息呢? 还是先发送消息,后update DB?
假设先update DB成功,发送消息网络失败,重发又失败,怎么办?
假设先发送消息成功,update DB失败。消息已经发出去了,又不能撤回,怎么办?
所以,这里下个结论: 只要发送消息和update DB这2个操作不是原子的,无论谁先谁后,都是有问题的。
那这个问题怎么解决呢?
错误的方案0
有人可能想到了,我可以把“发送消息”这个网络调用和update DB放在同1个事务里面,如果发送消息失败,update DB自动回滚。这样不就保证2个操作的原子性了吗?
这个方案看似正确,其实是错误的,原因有2:
(1)网络的2将军问题:发送消息失败,发送方并不知道是消息中间件真的没有收到消息呢?还是消息已经收到了,只是返回response的时候失败了?
如果是已经收到消息了,而发送端认为没有收到,执行update db的回滚操作。则会导致A账号的钱没有扣,B账号的钱却加了。
(2)把网络调用放在DB事务里面,可能会因为网络的延时,导致DB长事务。严重的,会block整个DB。这个风险很大。
基于以上分析,我们知道,这个方案其实是错误的!
方案1–业务方自己实现
假设消息中间件没有提供“事务消息”功能,比如你用的是Kafka。那如何解决这个问题呢?
解决方案如下:
(1)Producer端准备1张消息表,把update DB和insert message这2个操作,放在一个DB事务里面。
(2)准备一个后台程序,源源不断的把消息表中的message传送给消息中间件。失败了,不断重试重传。允许消息重复,但消息不会丢,顺序也不会打乱。
(3)Consumer端准备一个判重表。处理过的消息,记在判重表里面。实现业务的幂等。但这里又涉及一个原子性问题:如果保证消息消费 + insert message到判重表这2个操作的原子性?
消费成功,但insert判重表失败,怎么办?关于这个,在Kafka的源码分析系列,第1篇, exactly once问题的时候,有过讨论。
通过上面3步,我们基本就解决了这里update db和发送网络消息这2个操作的原子性问题。
但这个方案的一个缺点就是:需要设计DB消息表,同时还需要一个后台任务,不断扫描本地消息。导致消息的处理和业务逻辑耦合额外增加业务方的负担。
方案2 – RocketMQ 事务消息
为了能解决该问题,同时又不和业务耦合,RocketMQ提出了“事务消息”的概念。
具体来说,就是把消息的发送分成了2个阶段:Prepare阶段和确认阶段。
具体来说,上面的2个步骤,被分解成3个步骤:
(1) 发送Prepared消息;
(2) update DB;
(3) 根据update DB结果成功或失败,Confirm或者取消Prepared消息。
可能有人会问了,前2步执行成功了,最后1步失败了怎么办?这里就涉及到了RocketMQ的关键点:RocketMQ会定期(默认是1分钟)扫描所有的Prepared消息,询问发送方,到底是要确认这条消息发出去?还是取消此条消息?
具体代码实现如下:
也就是定义了一个checkListener,RocketMQ会回调此Listener,从而实现上面所说的方案。
// 也就是上文所说的,当RocketMQ发现`Prepared消息`时,会根据这个Listener实现的策略来决断事务
TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
// 构造事务消息的生产者
TransactionMQProducer producer = new TransactionMQProducer("groupName");
// 设置事务决断处理类
producer.setTransactionCheckListener(transactionCheckListener);
// 本地事务的处理逻辑,相当于示例中检查Bob账户并扣钱的逻辑
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
producer.start()
// 构造MSG,省略构造参数
Message msg = new Message(......);
// 发送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
producer.shutdown();
public TransactionSendResult sendMessageInTransaction(.....) {
// 逻辑代码,非实际代码
// 1.发送消息
sendResult = this.send(msg);
// sendResult.getSendStatus() == SEND_OK
// 2.如果消息发送成功,处理与消息关联的本地事务单元
LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
// 3.结束事务
this.endTransaction(sendResult, localTransactionState, localException);
}
总结:对比方案2和方案1,RocketMQ最大的改变,其实就是把“扫描消息表”这个事情,不让业务方做,而是消息中间件帮着做了。
至于消息表,其实还是没有省掉。因为消息中间件要询问发送方,事物是否执行成功,还是需要一个“变相的本地消息表”,记录事物执行状态。
人工介入
可能有人又要说了,无论方案1,还是方案2,发送端把消息成功放入了队列,但消费端消费失败怎么办?
消费失败了,重试,还一直失败怎么办?是不是要自动回滚整个流程?
答案是人工介入。从工程实践角度讲,这种整个流程自动回滚的代价是非常巨大的,不但实现复杂,还会引入新的问题。比如自动回滚失败,又怎么处理?
对应这种极低概率的case,采取人工处理,会比实现一个高复杂的自动化回滚系统,更加可靠,也更加简单。
分布式消息队列RocketMQ--事务消息--解决分布式事务的更多相关文章
- 分布式消息队列RocketMQ&Kafka -- 消息的“顺序消费”
在说到消息中间件的时候,我们通常都会谈到一个特性:消息的顺序消费问题.这个问题看起来很简单:Producer发送消息1, 2, 3... Consumer按1, 2, 3...顺序消费. 但实际情况却 ...
- 基于消息队列 RocketMQ 的大型分布式应用上云最佳实践
作者|绍舒 审核&校对:岁月.佳佳 编辑&排版:雯燕 前言 消息队列是分布式互联网架构的重要基础设施,在以下场景都有着重要的应用: 应用解耦 削峰填谷 异步通知 分布式事务 大数据处理 ...
- 分布式消息队列RocketMQ(一)安装与启动
分布式消息队列RocketMQ 一.RocketMQ简介 RocketMQ(火箭MQ) 出自于阿里,后开源给apache成为apache的顶级开源项目之一,顶住了淘宝10年的 双11压力 是电商产品的 ...
- 主流消息队列rocketMq,rabbitMq比对使用
首先整理这个文章是因为我正好有机会实战了一下rocketmq,阿里巴巴的一个开源消息中间件.所以就与以往中rabbitmq进行小小的比较一下.这里主线的根据常见面试问题进行整理. 一.消息队列常用的场 ...
- 分布式消息队列RocketMQ部署
一.RocketMQ简介: RocketMQ是一款分布式.队列模型的消息中间件,具有以下特点: 1.支持严格的消息顺序: 2.支持Topic与Queue两种模式: 3.亿级消息堆积能力: 4.比较友好 ...
- 聊一聊顺序消息(RocketMQ顺序消息的实现机制)
当我们说顺序时,我们在说什么? 日常思维中,顺序大部分情况会和时间关联起来,即时间的先后表示事件的顺序关系. 比如事件A发生在下午3点一刻,而事件B发生在下午4点,那么我们认为事件A发生在事件B之前, ...
- 分布式理论(4):Leases 一种解决分布式缓存一致性的高效容错机制(转)
作者:Cary G.Gray and David R. Cheriton 1989 译者:phylips@bmy 2011-5-7 出处:http://duanple.blog.163.com/blo ...
- 几种MQ消息队列对比与消息队列之间的通信问题
消息队列 开发语言 协议支持 设计模式 持久化支持 事务支持 负载均衡支持 功能特点 缺点 RabbitMQ Erlang AMQP,XMPP,SMTP,STOMP 代理(Broker)模式(消息在发 ...
- 分布式消息队列RocketMQ与Kafka架构上的巨大差异
分布式消息服务 Kafka 是一个高吞吐.高可用的消息中间件服务,适用于构建实时数据管道.流式数据处理.第三方解耦.流量削峰去谷等场景,具有大规模.高可靠.高并发访问.可扩展且完全托管的特点,是分布式 ...
随机推荐
- c++将数字转换成固定长度的字符串
c++将数字转换成固定长度的字符串 将数字转换为字符串,且设置为固定长度的,不足补零. string num2str(int i) { ]; sprintf(ss,"%04d",i ...
- CodeForces 727C
zsy: Guess the Array Time Limit:1000MS Memory Limit:262144KB 64bit IO Format:%I64d & %I64u Submi ...
- python 类编程相关内容(更新)
python作为面向对象的编程语言,类和对象相关的编程当然是少不了的! python类: class 类名 : 变量名 [ = 初始值 ] …… def 函数名 ( self [ , 其余参数列表 ] ...
- 经过强制类型转换以后,变量a, b的值分别为( )short a = 128; byte b = (byte) a;
1.Java中用补码形式表示 2.第一位正负位,1表示负,0表示正. 3.原码:一个数的二进制表示. 3的原码00000011 -3的 原码 10000011 4 ...
- zabbix——拓扑图入门
zabbix的拓扑图是动态的,当你配置好拓扑图后,一旦网络中那个节点出现了问题,很容易在拓扑图中看出问题具体出在哪个位置. 新建一个拓扑图 我们常用到的图标有rack(机柜)还有rackmountab ...
- hasura graphql-engine v1.0.0-alpha30 remote schema stitch 试用
新的hasura graphql-engine 代码仓库中已经包含了一个基于express 的简单graphql server, 可以用来测试模式拼接 graphql server 代码 项目结构 ├ ...
- vmware中扩充磁盘
刚开始装虚拟机中的系统时,只分了20G的空间大小,随着不断的使用,结果在安装一个软件的时候提示我磁盘空间不足 用"df -h "查看"/"目录使用率已达到90% ...
- 自动化部署--shell脚本--2
node1和node2都装apache [root@linux-node1 ~]# yum install httpd -y Loaded plugins: fastestmirror Loadi ...
- 使用kolla安装的openstack mariadb为集群所有节点无法启动
当在做测试时,把所有的openstack节点都关机,再开启做测试时,发现mariadb galera集群启不来,相当于所有的mariadb集群都停止了(跟所有节点断电情况相似),这时候怎么办呢,重新建 ...
- ES查询之刨根问底
昨天有一个需求,就是想要根据某个网关url做过滤,获取其下面所有的上下文nginx日志:如果直接"query":"https://XXX/YYY/ZZZ"发现有 ...