在分布式环境下,会涉及到多个服务以及数据库,比如支付库、商品库、订单库。不同的服务节点针对各自不同的数据库做操作想要保证一起成功或者一起回滚,就叫分布式事务。
两阶段提交(2PC):通过准备和提交阶段保证一致性,但有性能问题,也有数据不一致的风险
三阶段提交(3PC):在2PC的基础上添加了超时机制,降低了阻塞,但仍存在数据不一致的风险
TCC:Try、Confirm、Cancel
本地消息表:本地数据库执行完业务后,向本地消息表中插入一条消息,并通过定时任务处理消息的发送,发送或收到回应的时都会更新消息状态,也有重试机制
基于MQ的分布式事务:保证“本地事务”与“消息发送”的原子性,保证消息的可靠性。一个分布式事务由一系列本地事务组成,每个本地事务都会发布一个事件,触发下一个本地事务的执行。如果任何一个本地事务失败,就会触发一系列补偿事务来撤销之前已完成的操作。
XA协议负责参与者(各个数据库)和协调者(全局事务管理器)之间通信。
第一阶段是准备,第二阶段是提交。
2PC主要包含以下角色:
参与者将准备阶段的成败告诉协调者然后阻塞等待指令,但凡有一个参与者的操作失败,协调者会向所有的参与者发出回滚请求;如果全部的参与者都操作成功,协调者会向所有参与者发出commit指令
缺点:
CanCommit(参与者检查资源)、PrepareCommit(参与者锁定资源,但不提交,超时也会提交)、DoCommit/AbortCommit(协调者长时间没有收到ACK,会发送Abort)
三阶段提交解决的只是两阶段提交中单点故障和同步阻塞的问题,也会有数据不一致的问题,因为在3PC的第二阶段参与者等待超时会自动提交,而协调者在第三阶段如果检测到超时会Abort
是两阶段提交的变种,只不过2PC锁的是数据库资源,TCC通过改变业务中的资源的当前状态,锁的是业务逻辑中的资源:
Try:预留或锁定所需的业务资源,比如库存是否充足、帐户余额是否足够,但不操作或锁定数据库
Confirm:协调者发现所有的参与者Try过程都成功了,则向所有参与者发送Confirm请求,让所有参与者的提交事务
Cancel:协调者发现有参与者的Try过程失败,则向所有参与者发送Cancel请求
本地消息表的核心思想是将分布式事务拆分成本地事务进行处理。
将业务处理和后续调用其他服务的指令的消息(携带状态)的持久化放到一个本地事务中,要么业务成功且消息入库,要么都失败回滚。
一个独立的定时任务或服务会轮询本地消息表,将“待发送”的消息发送到 MQ。然后将消息状态修改为已发送
另一个服务执行成功,修改调用者的本地消息表中的对应消息状态为已成功或直接删除
失败则重试,保证最终一致性
本地消息表是将消息入库和业务逻辑合并为一个原子性事务,该事务提交后再异步轮询发送消息,MQ消息事务是本地事务提交后直接将消息提交到MQ,并有回查机制
生产者发送半消息: 生产者向 MQ 发送一个“半消息”(或称预提交消息),此时消息对消费者不可见。
执行本地事务: 生产者执行自己的本地数据库事务。
生产者通知 MQ 提交/回滚:
如果本地事务成功,生产者通知 MQ 提交半消息,消息变为可见。
如果本地事务失败,生产者通知 MQ 回滚半消息,消息被丢弃。
MQ 回查机制: 如果生产者在本地事务提交后但在通知 MQ 之前崩溃,MQ 会主动回查生产者,询问本地事务的最终状态,然后决定提交还是回滚半消息。
发送方完成并提交自己的本地事务后,尽最大的努力通知接受方结果,发送不成功就一致重试,直到最大次数。然后接受方再执行后续的业务逻辑。尽最大可能实现最终一致性。
我们用的是基于2PC的分布式事务解决方案Seata,因为自己实现分布式事务太麻烦了。
主要有三个角色:
具体过程是:
TM向TC申请一个开启全局事务,TC向TM返回一个代表全局事务的XID,RM负责执行当前的本地事务并向TC注册事务分支。当前本地事务执行完后,TM会调用下一个服务并传递XID,下一个服务的RM再执行相应的本地事务并向TC注册事务分支,以此类推。如果某个服务的业务执行过程中出现异常,当前服务的TM会向TC发起回滚请求,TC再协调全局的回滚。