分布式事务问题
通常单节点事务比较简单,Spring 提供的 @Transaction
注解能够实现。但是在分布式场景下,比如 ServiceA
调用 ServiceB
、ServiceC
,每个服务分别操作各自的数据库,如果某个服务调用成功、另外一个调用失败,就会造成数据的不一致性,这就是分布式事务问题。
2PC
二阶段提交是一种 强一致性、中心化的原子提交协议。
- 强一致性:体现在所有的参与者都能实时保持一致的数据、一致的状态。
- 中心化:分为协调者和参与者。
- 协调者负责协调整个事务的提交和回滚操作,负责推进分布式事务;比如上图的 ServiceA;
- 参与者是事务资源的拥有者,比如上图的 ServiceB、ServiceC;
- 原子提交:分布式事务中所有协调者要么全部成功,要么全部失败。
准备阶段
- 客户端发起一个分布式事务到协调者,协调者向所有的参与者广播
Prepare
请求; - 参与者在收到
Prepare
请求后会记录事务日志(RedoLog
、UndoLog
),然后根据这个事务需要锁定的资源尝试去执行,根据是否能够执行事务反馈协调者Yes/No
响应; - 协调者接收到所有参与者的响应,如果全部为
Yes
,就会进入提交阶段。 - 协调者如果接收到一个参与者的
No
响应,就会进入global rollback
阶段,进行全局回滚。
提交阶段
- 协调者通知所有参与者可以提交本次分支事务,发送
global commit
请求; - 参与者收到请求后,根据准备阶段记录的
RedoLog
日志,提交本地的分支事务; - 参与者在提交事务成功后,就会返回
ack
响应给协调者; - 协调者在接收到所有参与者成功的响应之后,返回给客户端成功。
全局回滚阶段
- 协调者向所有参与者发送
global rollback
请求; - 参与者收到请求后,会根据本地的
undoLog
回滚本地事务,回滚成功后返回给协调者ack
响应; - 协调者接收到所有
ack
响应之后,返回给客户端本次分布式事务执行失败。
隐藏问题
- 数据不一致问题:二阶段回滚或者提交阶段,如果某个参与者回滚或者提交失败了,协调者可以采取一个定期重试来确保所有参与者回滚成功。
- 同步阻塞问题:协调者必须等待所有参与者的
Yes/No
响应后,才能进入第二阶段;参与者必须等待协调者的global commit/global rollback
请求后才能进入第二阶段;如果协调者出现宕机了,那么所有参与者都无法释放事务资源。 - 单点问题/脑裂:整个分布式事务驱动都是基于协调者去做的,如果协调者出现问题,整个过程不可用。
3PC
三阶段提交是为了解决二阶段提交的 数据不一致、同步阻塞、单点等问题。
引入了超时机制:
- 参与者在等待协调者请求超时后,允许执行默认的操作;
- 协调者在等待参与者响应超时后,允许执行默认的操作(发送中断事务)。
降低了事务资源的锁定范围:
- 新增了
CanCommit
阶段,不会像 2PC 一样一开始就锁定所有事务资源,而是排除掉个别不具备处理事务能力的参与者的前提下,再进入二阶段。
协商过程
区别于 2PC,只是在第一步先由协调者向所有参与者发送 CanCommit
请求,根据参与者的响应排除一些不具备事务处理能力的参与者,后续阶段和 2PC 相同。
3PC 相比 2PC 的优化
- 增加了
CanCommit
阶段来减少事务资源的锁定范围,排除掉一些不具备事务资源能力的参与者; - 降低了同步阻塞,分别在参与者等待协调者请求、协调者在等待参与者响应 两个阶段新增超时机制。比如说参与者在等待协调者响应超时后默认执行提交操作,因为经过
CanCommit
阶段后事务成功提交的可能性已经很大了。
3PC 引入的问题
- 新增一轮消息,增加了复杂度和协商效率;
- 数据不一致:如果第二阶段结束后,一部分参与者能够执行事务反馈
Yes
,另一部分参与者不能执行事务反馈No
,此时协调者恰好宕机,那么参与者会因为等待超时而自动执行默认操作,这就导致了部分事务成功提交、部分事务回滚,产生数据不一致的问题。