前言
- 在当下微服务盛行时代, 随着单体应用根据业务拆分为多个应用, 虽提高了可用性等优势, 但成本, 事务等问题也凸显了出来
正文
- 本地事务
- 把一个数据库内部的事务处理, 如对多个表的操作, 作为本地事务看待;
- 分布式事务(全局事务)
- 多个数据库可能需要共同完成一个工作;
两阶段提交(2PC)
- 保证分布式事务的原子性;
- 两阶段分别指:
准备,提交; - 参与者将操作成败通知协调者, 再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作
准备
- 事务协调者(事务管理器)给每个参与者(资源管理器)发送准备(Prepare)消息, 参与者返回失败或执行本地事务(写本地的 Redo(物理日志)和 Undo(逻辑日志), 但不提交(万事俱备,只欠东风的状态));
- 协调者向各参与者发起
询问是否可以执行提交操作, 同步等待各参与者响应; - 各参与者执行到
询问为止的所有事务操作, 并将 Undo 信息和 Redo 信息写入日志(!: 若成功,这一步各参与者其实已执行了事务操作). - 各参与者响应协调者的
询问指令, 如果各参与者的事务操作已执行成功, 则返回‘同意’, 如果有参与者的事务操作执行失败, 则返回‘中止’;
- 协调者向各参与者发起
提交
- 协调者根据参与者的反应情况来决定是否继续事务的
Commit请求或者Abort请求. 各参与者根据协调者的指令执行‘Commit’或‘Abort‘操作, 释放所有事务处理过程中使用的’锁资源’(!: 必须在最后阶段释放锁资源).-
同意
- 协调者从各参与者收到的响应均是‘同意’;
- 发送提交请求: 协调者向各参与者发送
Commit请求; - 事务提交: 参与者收到
Commit指令后, 正式完成操作, 释放在整个事务期间占用的资源; - 反馈结果: 参与者完成
Commit后, 向协调者发送‘ACK’响应; - 完成事务: 协调者收到各参与者反馈的‘ACK’消息后, 完成事务;
- 发送提交请求: 协调者向各参与者发送
- 协调者从各参与者收到的响应均是‘同意’;
-
中止
- 协调者从某参与者收到的响应是‘中止’或者协调者在
询问后, 超时之前无法得到所有参与者的响应;- 发送中断请求: 协调者向各参与者发送
Abort请求 - 事务回滚: 参与者收到
Abort指令后, 利用之前写入的 Undo 信息执行回滚, 释放在整个事务期间占用的资源; - 反馈结果: 参与者完成
回滚后, 向协调者发送‘ACK’响应; - 中断事务: 协调者收到各参与者反馈的‘ACK’消息后, 执行事务的中断;
- 发送中断请求: 协调者向各参与者发送
- 协调者从某参与者收到的响应是‘中止’或者协调者在
-
- 不论结果如何,
提交阶段都会结束当前事务.
缺点
- 同步阻塞, 执行过程中, 各参与者都是事务阻塞型的. 当参与者占用公共资源时, 其他节点访问公共资源就不得不处于‘阻塞’状态;
- 单点故障, 协调者的重要性, 如果协调者发生故障, 参与者将一直阻塞下去, 尤其在
提交阶段, 协调者发生故障, 各参与者还都处于‘锁定事务资源’状态中, 无法继续完成事务操作.(协调者挂掉,可重新选举一个协调者, 但无法解决因协调者故障导致的参与者处于‘阻塞’状态的问题); - 数据不一致, 在
提交阶段, 协调者向各参与者发送正式提交(Commit)请求后, 发生‘局部网络异常’或协调者发生‘故障’, 将导致一部分参与者接受了正式提交(Commit)请求, 这些参与者将正常执行, 但其余参与者因未收到正式提交(Commit)请求而无法执行‘事务提交’, 结果便是分布式系统出现‘数据不一致’现象; - 协议成本高, 存在全局锁问题;
无法解决的问题
- 协调者在发出
正式提交(Commit)请求后宕机, 而收到‘请求’的参与者也同时宕机, 那么即使协调者通过选举产生新的协调者, 这个事务的状态也将是不确定的, 没人知道是否已提交;
结果
- 两阶段提交存在着同步阻塞,单点故障,脑裂等缺陷;
三阶段提交(3PC)
- 两阶段提交的改进版本;
区别
- 引入超时机制(协调者和参与者都引入超时机制);
- 在第一阶段和第二阶段中插入一个准备阶段, 保证在最后
提交阶段之前各参与者状态一致;
CanCommit
- 协调者向各参与者发送
CanCommit请求;- 事务询问: 协调者向各参与者发送
CanCommit请求, 询问是否可以执行提交(Commit)操作, 然后等待各参与者响应; - 响应反馈: 参与者收到
CanCommit请求后, 正常情况下, 如果其自身认为可以顺利执行事务, 则返回‘同意’, 并进去预备状态; 反之返回‘中止’;
- 事务询问: 协调者向各参与者发送
PreCommit
- 协调者根据参与者的反应情况来决定是否可以继续事务的
PreCommit请求- 同意
- 协调者从各参与者收到的响应均是‘同意’;
- 发起预提交: 协调者向参与者发送
PreCommit请求, 并进入 Prepared 阶段; - 事务预提交: 各参与者收到
PreCommit指令后, 执行事务操作, 并将 Undo 和 Redo 信息记录到日志中; - 响应反馈: 各参与者成功执行了事务操作, 返回 ACK 响应, 等待最终指令;
- 发起预提交: 协调者向参与者发送
- 协调者从各参与者收到的响应均是‘同意’;
- 中止
- 协调者从某参与者收到的响应是‘中止’或者协调者在
PreCommit请求后, 超时之前无法得到所有参与者的响应;- 发送中断请求: 协调者向参与者发送
Abort请求; - 中断事务: 各参与者收到
Abort指令后(或超时后,仍未收到协调者的请求), 执行事务中断;
- 发送中断请求: 协调者向参与者发送
- 协调者从某参与者收到的响应是‘中止’或者协调者在
- 同意
DoCommit
- 真正的事务提交, 分两种情况;
- 执行提交
- 发送提交请求: 协调者收到各参与者的 ACK 响应后, 协调者将从‘预提交’进入到‘提交’状态, 向各参与者发送
Commit请求; - 事务提交: 参与者收到
Commit指令后, 正式完成操作, 释放在整个事务期间占用的资源; - 反馈结果: 参与者完成
Commit后, 向协调者发送‘ACK’响应; - 完成事务: 协调者收到各参与者反馈的‘ACK’消息后, 完成事务;
- 发送提交请求: 协调者收到各参与者的 ACK 响应后, 协调者将从‘预提交’进入到‘提交’状态, 向各参与者发送
- 中断事务
- 发送中断请求: 协调者向各参与者发送
Abort请求; - 事务回滚: 参与者收到
Abort指令后, 利用之前写入的 Undo 信息执行回滚, 释放在整个事务期间占用的资源; - 反馈结果: 参与者完成
回滚后, 向协调者发送‘ACK’响应; - 中断事务: 协调者收到各参与者反馈的‘ACK’消息后, 执行事务的中断;
- 发送中断请求: 协调者向各参与者发送
- 执行提交
注意
DoCommit阶段, 若参与者无法及时收到来自协调者的Commit或Abort请求是, 超时后会自动进行Commit操作.
2PC 与 3PC 区别
- 3PC 主要解决 2PC 的单点故障, 同步阻塞问题. 因参与者无法及时收到协调者的请求后会默认
Commit, 而不会一直持有事务资源并处于‘阻塞状态’, 但这种机制有时也会导致数据一致性问题(因网络等其他情况,协调者发送Abort请求没有及时被参与者接受到, 那么参与者在等待超时后执行了Commit指令, 这样就和其他参与者之间存在数据不一致性问题);
BASE
- 各参与者不一定同时在线, 允许系统状态更新延迟, 这个延迟对客户来说不一定能察觉, 实现最终一致性.
TCC
- 基于两阶段提交的分布式事务实现方案
Try
- 尝试执行业务
- 完成所有业务检查, 预留必要的业务资源
Confirm
- 确认执行业务
- 真正执行业务, 不做业务检查
Cancel
- 取消执行业务
- 释放 Try 阶段预留的业务资源
2PC 与 TCC 区别
- TCC 位于业务服务层, 没有单独的准备阶段, Try 操作可以灵活的选择业务资源锁的粒度;
- TCC 通过最终一致性来解决系统性能问题;
优点
- 隔离型高
缺点
- 增大编码量
适用场景
- 基础服务
- 中台服务
Saga
- 长活事务, 由多个‘本地事务(sub-transaction Ti)’组成, 每个‘本地事务’均有相应
执行模块,补偿模块(Ci), 任意一个‘本地事务’出错, 可通过调用对象补偿方法恢复, 达到事务的最终一致性;
实现方式
- 集中式
- 通过一个 Saga 对象来追踪所有 Saga 子任务的调用情况, 根据调用情况来决定是否需要调用对应的’补偿模块’(协调器和调用方在一个进程中).
- 集中式实现方式直观, 容易控制. 业务耦合度高;
- 分布式
- 采用’事件驱动’方式让各参与者进行相互交互, 相关业务只需订阅相关领域事件即可.
- 分布式降低系统复杂程度, 提高系统拓展性. 如果参与者过多, 会对编码, 调试带来问题; 业务逻辑是基于事件, 有可能会有循环依赖的问题;
补偿策略
- 向后恢复(backward recovery): 补偿所有已完成的事务. 如果一’本地事务(j)‘失败, 撤销之前所有已成功的’本地事务’(之前 Saga 所有执行结果撤销). (例, T2, …, Tj, Cj, …, C2, C1 (0 < j < n))
- 向前恢复(forward recovery): 重试失败的事务. 使用于必须要成功的场景. (T1, T2, …, Tj(fail), Tj(retry), Tj+1, …, Tn)
ACID
- 原子性: 通过’Saga 协调器’实现;
- 一致性: 通过’本地事务’ + ‘Saga log’ 保证;
- 隔离性: 不保证;
- 持久性: 通过’Saga log’ 保证;
- 总结: Saga 只支持 ACD, 不支持 I(隔离性)的保证;
要求
- 请求幂等: 因网络请求等各种原因, 服务间请求时会出现’超时重试’的情况, 需要通过’幂等’来避免多次请求所带来的问题;
- 事务数据:
注意
- 因 Saga 事务没有’准备’阶段, 所以事务没有隔离, 操作过程中若出现多’本地事务’同时操作一资源, 有可能会出现数据语义不一致, 更新丢失, 脏读取, 模糊读取等各种问题.
- 可参考 TCC, 在业务层加入 Session, 锁机制来保证串行化操作资源;
- 业务层操作冻结(隔离)资源方式, 保证各’本地事务’操作不至于影响其他’本地事务’;
- 业务操作过程中通过及时读取当前状态的方式获取到最新的更新;
适用场景
- 其他组织业务
- 遗留系统
总结
微服务
- 服务内: 内刚, 通过数据库事务保证强一致性;
- 服务间: 外柔,
If you enjoyed this, leave a comment~