Rocket MQ如何支持分布式事务,看到一篇很好的博文。
一、前言
1、什么是事务?
要么都成功,要么都失败。
2、什么是本地事务?
通过数据库事务的特性来控制。依赖于数据库。基于关系型数据库的事务,称为本地事务。
3、事务四大特性:ACID(原子性、一致性、隔离性、持久性)。
4、什么是分布式事务?
单体系统拆分成若干个服务,即微服务的由来。服务与服务之间需要通过网络远程协作。
例如增加用户送积分。需要用户服务和积分服务。
例如张三给李四转账。张三的账户和李四的账户属于两个银行。
5、产生场景?
跨JVM进程。
单体系统跨数据库。
多个微服务连接同一个数据库。(也是跨JVM进程)
二、基础理论
1、CAP理论。
Consistency:一致性(数据写入成功之后去读取,可以读到最新的数据。)
Available:可用性(所有的操作都能得到响应,不能响应超时或失败。)
Paitition:网络容忍性(一个节点挂掉了不能影响其他节点提供服务。)
例:主从模式,写到主,需要从主同步到从,会有延迟。
一致性的特点:
a、由于存在数据同步的过程,写操作的响应会有一定的延迟。
b、为了保证数据一致性会对资源暂时锁定,待数据同步完成后释放锁定资源。
c、如果请求数据同步失败的节点则会返回失败信息,不能返回旧数据。
可用性的特点:
所有的操作都能得到响应,不会响应超时或响应错误,可以返回旧数据。
网络容忍性的特点:
分布式系统,不同的服务部署到不同的子网,服务之间不能进行通信了,仍然能够对外提供服务。期中一个节点挂掉了,不能影响别的节点提供服务。分布式系统难免会产生网络分区,是分布式系统的基本能力。
CAP组合方式:
不能同时满足C、A、P,只能同时满足两个。
当满足P的情况下,C和A不能同时满足。
AP组合:查到旧数据,保证可用。(最常用)
CP组合:放弃可用性,追求一致性。
CA组合:放弃了分区容忍性,就不是分布式系统了。
2、BASE理论
BA基本可用、S软状态、E最终一致性。
BA基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
S软状态:不要求强一致性,允许系统中存在中间状态,例如支付中、处理中等,待数据最终一致后状态修改为成功。
E最终一致性:经过一段时间后,所有节点的数据达成一致。
注意强一致性和最终一致性的区别。
三、解决方案
1、2PC
即两阶段提交协议。将整个事务流程分为两个阶段,准备阶段和提交阶段。
角色:事务管理器、事务参与者(多个)。
准备阶段:事务管理器给每个参与者发送prepare消息,每个本地数据库事务参与者在本地执行事务,并写本地的undo log(记录修改以前的数据,回滚使用)和redo log(记录修改后的数据,用于提交事务后写入数据文件)。
提交阶段:如果事务管理器收到了事务参与者执行失败或者超时的消息,直接给每个参与者发送rollback消息;反之,发送commit消息。
1、2PC解决方案之XA方案
基于数据库的2PC协议实现。依赖关系型数据库。资源锁要等所有执行完成之后才释放,效率低。
例如增加用户送积分。
在准备阶段RM执行实际的业务操作,但不提交事务,资源锁定。
在提交阶段TM会接受RM在准备阶段的执行回复,只要有任一个RM执行失败,TM会通知所有RM执行回滚操作,否则,TM会通知所有RM执行提交操作,提交阶段结束,资源锁释放。
2、2PC解决方案之seata方案
基于应用层实现。
seata的设计思想:对业务无侵入。把一个分布式事务理解成了包含若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务,要么一起成功提交,要么一起失败回滚。通常一个分支事务就是一个关系数据库事务。
事务协调器:是独立的中间件,需要独立部署运行。
事务管理器:嵌入应用程序中(一个jar包),负责开启一个全局事务,并最终向事务协调器发起全局提交或全局回滚的指令。
事务参与者:分支事务参与者。
传统2PC和seata 实现2PC的区别。
1、seata实现2PC事务。
实现张三向李四转账的事务控制。
需要引入seata的依赖;下载并运行seata协调器;张三微服务;李四微服务;注册中心。
seata代理会生成或删除undo log记录。
2、TCC
try、confirm、cancel。
try成功,confirm必须成功;try失败,需要回滚。
空回滚:没有调用try方法就调用了第二阶段的回滚。
解决空回滚:try执行,记录一下。空回滚的时候判断一下。
幂等:重复执行,不能影响结果。
解决幂等:也是记录执行。
悬挂:先回滚再预留资源。如果第二个阶段已经执行,就不要再执行了。
解决悬挂:也是记录执行。
1、解决方案之Hmily
3、可靠消息最终一致性
可靠消息最终一致性方案是指事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功。此方案强调的是只要消息发给事务参与方最终事务要达成一致。
此方案是利用消息中间件完成。
强调两个方面:
1、发起方给消息中间件发送消息。
2、消费方要保证最终一致。
例如:张三给李四转账,张三账号要扣减金额,并且必须把消息发送给消息中间件。李四消费了消息,李四账户必须要增加金额。
事务发起方(消息生成者)将消息发给消息中间件,事务参与方(消息消费者)从消息中间件接收消息。事务发起方和消息中间件、实物参与方和消息中间件,都是通过网络通信,由于网络通信的不确定性可能造成分布式事务问题。
可能产生的问题:
1、本地事务与消息发送的原子性问题。
即张三账户扣减金额,同时消息必须发送到消息中间件。
代码分析:
// 开启事务
// 张三扣减金额
// 发送消息到消息中间件。(会出现的问题,消息发送到了消息中间件,但由于网络的问题事务超时,张三扣减金额做了回滚。)
// 事务结束
2、事务的参与方接收消息的可靠性。
事务参与方必须能够从消息中间件接收到消息,如果接收消息失败可以重复接收消息。
例如:李四可以从消息中间件接收到消息并进行李四账户增加金额。如果第一次接收消息失败,可以重复进行接收消息。
3、消息重复消费的问题。
由于网络问题的存在,某一个消费节点超时但已经消费了消息,消息中间件会重复给该消费节点发送消息,造成重复消费的问题。
例如:消息中间件给李四微服务发送消息,李四微服务已经成功消费了消息并增加了李四账户的金额,但是由于网络超时,消息中间件不知道李四已经成功消息了消息,会重复给李四微服务发送这条消息。解决方法,要实现消息消费的幂等性。
1、解决方案之本地消息表
本地数据库添加一张表,记录消息的发送和消费。
场景:增加用户送积分。涉及到两个微服务,用户服务和积分服务。用户服务的用户表要增加一条记录,用户服务本地再增加一张积分消息日志表。
本地事务代码如下:
// 用户表新增一条记录
// 积分消息日志表增加一条记录
本地服务开启一个定时任务:
// 用户服务启动一个定时任务,扫描积分消息日志表。
积分服务监听消息队列。消费消息了一定要给消息中间件一个回应,即消息中间件的消息确认机制(即ack机制。如果不给消息中间件回应,消息中间件会一直发送该条消息):
// 积分服务消费消息。
2、解决方案之RocketMQ
Rocket MQ 4.3之后的版本支持分布式事务消息。
Rocket MQ从消息发出到消息消费的流程:
角色:消息生产者、Rocket MQ Server、消息消费者。
为了防止由于网络问题步骤4执行失败,MQ可以回查事务(步骤5)。
Rocket MQ提供了RocketMQLocalTransactionListener接口,该接口中有事务回查方法。
本地消息表和Rocket MQ两种解决方案,推荐使用Rocket MQ,因为本地消息表要在本地多增加一张表,要开启定时任务。