XA协议
XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。因此,XA 协议可以分为两部分,即事务管理器
和本地资源管理器
。
事务管理器作为
协调者,负责各个本地资源的提交和回滚
;
资源管理器就是分布式事务的参与者.其中资源管理通常是数据库
。
2PC(Two-phase Commit)
原理
分为投票
和提交
两个阶段。
投票为第一阶段:
协调者(Coordinator,即事务管理器)
会向事务的参与者(Cohort,即本地资源管理器)
发起执行操作的CanCommit
请求,并等待参与者的响应.- 参与者接收到请求后,会执行请求中的事务操作,记录日志信息(包含事务执行前的镜像),同时锁定当前记录。参与者执行成功,则向协调者发送
“Yes”
消息,表示同意操作;若不成功,则发送“No”
消息,表示终止操作。- 当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了提交阶段。
提交第二阶段:
- 协调者会根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令
- 若协调者收到的都是“Yes”消息,则向参与者发送
“DoCommit”
消息,参与者会完成剩余的操作并释放资源,然后向协调者返回“HaveCommitted”
消息;- 如果协调者收到的消息中包含“No”消息,则向所有参与者发送
“DoAbort”
消息,此时发送“Yes”的参与者则会根据之前执行操作时的回滚日志对操作进行回滚,然后所有参与者会向协调者发送“HaveCommitted”
消息;- 协调者接收到“HaveCommitted”消息,就意味着整个事务结束了
问题
同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的
。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态
。
协调者单点故障导致参与者长期阻塞问题:基于 XA 的二阶段提交算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态
。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞
。
数据不一致问题:在提交阶段,当协调者向参与者发送 DoCommit 请求之后,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作
,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题
。
二阶段无法解决的问题:协调者在发出DoCommit 消息之后宕机
,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的
,没人知道事务是否被已经提交。
3PC(Three-phase commit protocol)
三阶段提交协议(Three-phase commit protocol,3PC),是对二阶段提交(2PC)的改进。为了解决两阶段提交的同步阻塞(但二阶段到三阶段仍然是同步阻塞),三阶段提交引入了
超时机制和准备阶段`。
超时机制:同时在协调者和参与者中引入超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务
。
准备阶段:在第一阶段和第二阶段中间引入了一个准备阶段
,也就是在提交阶段之前,加入了一个预提交阶段
。在预提交阶段排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。
CanCommit 阶段
协调者向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应;参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。
PreCommit 阶段
协调者根据参与者CanCommit 阶段
的回复情况,来决定是否可以进行PreCommit
操作或中断事务。
DoCommit 阶段
协调者根据参与者PreCommit 阶段
的回复情况,来决定是否可以进行DoCommit
操作或中断事务。
TCC(Try-Confirm-Cancel)
国内自己开源出去的:ByteTCC,TCC-transaction,Himly。
预处理Try
、确认Confirm
、撤销Cancel
。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作既回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试
。
TCC分为三个阶段 :
Try阶段:做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm一起才能真正构成一个完整的业务逻辑。
Confirm阶段:做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即 :只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理
。
Cancel阶段:是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理
。
TM事务管理器
TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用组件,是为了考虑结构和软件复用。
TM在发起全局事务时生成全局事务记录
,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录
状态,由于Confirm和Cancel失败需进行重试,因此需要实现为幂等性是指同一个操作无论请求多少次,其结果都相同
。
消息事务
消息队列中的事务主要解决的是消息生产者和消息消费者的数据一致性问题
RocketMQ
RocketMQ 事务消息的实现原理基于两阶段提交和定时事务状态回查
来决定消息最终是提交还是回滚,RocketMQ 先执行第一部分的事务,如果失败则回滚,如果成功则定时任务会去回查到事务执行成功,这个时候通知消费者执行第二阶段的事务,如果失败则不断重发消息给消费者消费,如果成功则整个流程走完,保证了事务的原子性。
- Producer发送
half message
给RocketMQ - RocketMQ返回
half message success
(半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说,这个消息是不可见的,因为消息存储在Topic为RMQ_SYS_TRANS_HALF_TOPIC
的消息消费队列中,而不是原先的Topic) - 执行核心交易链路
- 返回执行交易链路的结果,如果失败则回滚
- 如果执行成功,则Producer返回一个
COMMIT
状态给RocketMQ - RocketMQ提供
事务反查来解决异常情况
,如果RocketMQ没有收到提交或者回滚的请求,broker会定时到生产者上去反查本地事务的状态
,即这条消息的状态为UNKNOWN
,则会回调服务接口,查询这条消息到底是commit还是rollback - RocketMQ确认消息为commit,则Consumer可以消费到这条消息
- Consumer操作数据库,执行自己的事务
- Consumer成功消费之后返回一个ACK消息给RocketMQ,如果成功消费则显示消费成功,否则RocketMQ会重发消息给Consumer继续消费(
producer不会因为consumer消费失败而做回滚
)
kafka
kafka事务也是基于二阶段提交
来实现的,但是它在其中加入了一个事务协调者
的概念,事务协调者并不是独立的应用,它是broker的一部分,协调者也是需要ZooKeeper来保证其可用性的,为了提高并行性能,kafka允许多个协调者分别负责管理事务主题中的不同的分区
。
开启事
务,生产者发送给协调者一个事务开启请求,协调者在事务日志中创建一个事务ID并记录
。- 生产者发消息之前给协调者发送消息定
位通知(发送的消息属于哪个主题和分区)
,协调者记录消息定位消息。- 生产者将一些真正的业务消息给对应分区的broker,kafka客户端会自动过滤未提交的事务消息。
- 生产者发送消息后,
生产者给协调者发送提交或者回滚事务的请求
。协调者根据结果将事务状态设置为回滚或者预提交状态,并写入日志。- 协调者在事务相关的所有分区中写入一条“事务结束”的消息,当消费者收到这个消息时候,就可以把之前那些过滤掉未提交的消息放开给消费者消费,协调者记录日志,事务结束。
Q:如果你当前使用的消息队列
不支持“半消息/预发消息”
怎么做?
A: 可以使用关系型数据库的一行记录来记录本地事务
,使用状态列来表示本地事务执行的结果,通过异步线程不断捞出本地事务执行成功的消息发生到MQ中
。
Q:为什么要增加一个
消息预发送机制
,增加两次发布出去消息的重试机制,为什么不在业务成功之后,发送失败的话使用一次重试机制?
A:如果业务执行成功,再去发消息,此时如果还没来得及发消息,业务系统就已经宕机了,系统重启后,根本没有记录之前是否发送过消息,这样就会导致业务执行成功,消息最终没发出去的情况
。
Q:如果consumer端因为业务异常而导致回滚,那么岂不是两边最终无法保证一致性?
A:我们上面提到过在分布式事务中只允许系统异常失败,不允许业务异常失败
。我们可以通过重试
来实现最终一致性
,或者通过监控差错系统
来单独处理这类问题。两阶段提交的方式是不适合强一致性的业务场景
。
Q:kafka事务消息和RocketMQ事务消息的区别?
A:它们实用的场景是不一样的,RocketMQ
中的事务,它解决的问题是,确保执行本地事务和发消息这两个操作,要么都成功,要么都失败
。并且RocketMQ增加了一个事务反查的机制
,来尽量提高事务执行的成功率和数据一致性
。Kafka
中的事务,它解决的问题是,确保在一个事务中发送的多条消息,要么都成功,要么都失败
。(这里面的多条消息不一定要在同一个主题和分区中,可以是发往多个主题和分区的消息)当然也可以在kafka事务执行过程中开启本地事务来实现类似RocketMQ事务消息的效果,但是Kafka是没有事务消息反查机制的,它是直接抛出异常
的,用户可以根据异常来实现自己的重试等方法保证事务正常运行。
Q:kafka的
Exactly Once
语义怎么理解?
A:一般情况下我能使用kafka事务消息配合幂等机制来实现Exactly Once(仅且一次)
,这里的Exactly Once和消息对列服务中的“仅且一次”概念是完全不一样的。在消息队列的“仅且一次”是指从生产者发送消息到Broker,再到消费者消费到消息仅且一次
。而kafka中的Exactly Once它解决的是在流计算中kafka作为数据源并将计算结果保存到kafka中的这个过程,保证每条消息都仅且一次被计算过
。