在保证分布式事务一致性的过程中我们会遇到什么问题?
约 3040 字大约 10 分钟
2025-01-09
在微服务架构盛行的今天, 如何保证分布式事务的一致性是每一个后台开发工程师都可能遇到的问题.
虽然对于分布式事务的一致性已经有非常成熟的解决方案(例如: 2PC, 3PC, TCC 等), 但是我发现很少有文章讲解这其中可能会遇到的问题, 也就是每一种解决方案存在的缺陷. 事实上, 这些缺陷正是工程师们必须注意的, 这关系到了系统的稳定以及你可能在生产环境中遇到的问题.
本文会用比较简单的语言介绍每种解决方案, 然后着重讨论他们的缺陷, 以及如何改进.
为什么需要分布式事务?
数据库中我们将一个或一组 SQL 组成的操作叫做事务, 每个事务要么全部成功, 要么全部失败. 而这一切通常依靠数据库本身进行保障, 常见的关系型数据库如: MySQL, PostgreSQL 都会保证事务的原子性.
但是在微服务的架构中所有的业务数据并不全部保存在同一个子服务中, 即数据不在同一个数据库集群中. 这个时候数据库只能保证其本身的事务原子性, 而无法控制在另一个服务器中的数据库的事务操作. 这种在同一个业务操作中涉及到操作两个不同子服务的数据库的事务我们称为: 分布式事务. 而正因为分布式事务涉及到多个独立的数据库, 所以在操作过程中由于部分的操作失败可能会出现不同数据库之间的数据不一致.
例如我们现在有一个电商系统, 其中包含两个子服务: 库存管理 和 订单系统. 当用户做出一个"下单"的业务操作时, 库存管理服务需要将商品库存减一, 订单系统需要生产新的订单信息. 这个时候如果订单信息生成成功, 而库存减一的操作受到网络或者其他影响操作失败, 则会出现分布式事务的不一致性.
本文接下来的内容会介绍解决这种不一致性的方法, 以及这些方法的弊端.
2PC
算法原理
2PC 又被称为: 两段式提交. 即将一次分布式事务的提交拆分为 Prepare 和 Commit 两个阶段. 整个系统中存在两个角色: 一个协调者 和 若干个参与者. 协调者负责发布事务并控制算法进行的阶段. 参与者负责执行具体的 SQL 事务并将执行的结果反馈给协调者.
我们现在来分别看一下协调者和参与者在两个阶段中都会做什么.
Prepare
当我们启动一个分布式事务的时候, 2PC 算法会进入 Prepare
阶段, 所有的参与者会在本地尝试执行本次事务.
协调者发布事务命令给所有的参与者.
参与者锁定资源并预执行命令.
返回执行结果给协调者. 返回的结果可能是成功, 也可能是失败.
Commit
当第一个阶段 Prepare
结束的时候, 协调者会接收到所有参与者的反馈信息, 即它们自己的本地事务是否执行成功. 这个时候协调者会根据所有的这些反馈信息发布 Commit
阶段的命令. 如果所有参与者都执行成功, 则发布 commit
命令让所有参与者都正式提交自己的本地事务. 如果有任何一个参与者执行失败, 则发布 rollback
回滚命令让所有参与者都放弃已经本地执行的事务, 将数据回退到整个分布式事务开始前的样子.
协调者查看所有参与者的反馈信息.
如果全部成功则发布
commit
命令给所有参与者.如果有任何一个参与者失败则发布
rollback
命令给所有参与者.
缺陷
性能问题.
在执行过程中所有参与的节点都是事务阻塞型的, 参与者锁定公共资源后第三方访问的时候不得不处于阻塞的状态.
单点故障.
一旦协调者发生故障, 参与者会一直阻塞下去, 无法完成事务的操作.
数据不一致.
在二阶段的处理中, 如果在事务协调者向参与者发送
commit
请求后出现局部网络异常, 导致只有部分参与者收到了请求, 使得整个分布式系统出现数据不一致的情况.状态不确定.
如果协调者在发出
commit
之后宕机, 且唯一接收到这个commit
的参与者也宕机, 那么即使选出了最新的协调者也无法确定事务的状态.
3PC
3PC 又被称为: 三段式提交, 是对 2PC 的一次改进. 除了性能上有所提升以外还避免了一定的单点故障导致的无限阻塞.
在 3PC 中, 我们在 Prepare
和 Commit
这两个阶段之前增加一个一个新的阶段: CanCommit, 并增加了超时机制.
算法原理
CanCommit
协调者发起分布式事务.
参与者尝试获取数据库锁, 检查是否具备完成事务的能力.
如果参与者认为自身可以完成事务, 则返回 YES, 否则返回 NO 给协调者.
此阶段内参与者只是尝试获取数据库锁, 但是并没有一直持有数据库锁, 所以不会造成资源的长期锁定.
PreCommit
假如协调者收到了所有参与者的反馈都是 YES, 则进入 PreCommit
阶段进行事务的预提交.
协调者向所有参与者发送
PreCommit
请求.参与者开始执行事务操作, 并将 Undo 和 Redo 信息记录到事务日志.
参与者向协调者反馈 Ack 以标识准备好提交并等待其下一步的指令.
此时参与者的事务还处于未提交的状态.
如果 CanCommit 阶段协调者受到任一参与者返回的 No 响应, 或者在等待参与者返回的过程中 超时, 则触发异常流程, 整个分布式事务将会中断.
协调者向所有参与者发送
abort
命令.参与者放弃本次事务.
DoCommit
假如协调者收到了所有参与者的 Ack, 则进入 DoCommit
阶段进行事务的正式提交.
协调者向所有参与者发送
DoCommit
请求.参与者开始提交事务操作.
参与者在提交成功后返回 Ack 给协调者.
如果 PreCommit
阶段协调者受到任一参与者返回的 No 响应, 或者在等待参与者返回的过程中 超时, 则触发异常流程, 整个分布式事务将会中断.
协调者向所有参与者发送
abort
命令.参与者放弃本次事务.
相较于 2PC 的优化
超时机制.
3PC 引入了参与者的超时机制, 可能发生在
PreCommit
和DoCommit
阶段. - 参与者在PreCommit
阶段发生超时会放弃执行,DoCommit
阶段发生会执行提交. - 协调者在任何时候发生超时都会发送回滚.这避免了 2PC 中参与者在长时间无法与协调者通信或者协调者宕机的情况下无法释放资源的问题, 即单点故障问题.
性能优化.
3PC 增加了一个
CanCommit
阶段, 参与者在此时并不持有数据库锁. 3PC 减少了参与者持有锁的时间.
缺陷
数据一致性问题.
3PC 中仍然没有完全解决由于局部网络问题而导致的数据的不一致问题.
TCC
TCC(Try-Confirm-Cancel) 又称为: 补偿事务. 其核心思想是: 针对每个操作都要注册一个与其对应的确认和补偿. 不同于 2PC 和 3PC, TCC 引入了一个事务协调服务进行调度.
算法原理
Try 阶段
对业务系统进行检测, 进行资源预留.
Confirm 阶段
确认执行业务操作.
Cancel 阶段
取消执行业务操作.
在绝大多数的金融场景中都是使用 TCC 方式来保证分布式事务的一致性的. 因为在这种更加严谨的业务场景中, 我们需要对每一种业务流程有更高的控制.
缺陷
业务侵入性强.
必须为每一个业务逻辑都实现 Try, Confirm 和 Cancel 三个操作的对应实现.
实现难度大.
需要按照不同的失败原因(例如: 网络故障, 系统故障等)实现不同的回滚策略.
异步
上面介绍的所有解决方案都是同步的调用方案, 当并发量特别大的时候性能并不理想. 我们可以通过借助消息队列来进行系统解耦和消息缓存, 以此达到更高的并发性能.
算法原理
我们将分布式的操作分为: 上游业务和下游业务, 并创建一个消息管理服务.
核心思想为: 上游业务和消息管理服务之间存在一个 2PC 的机制来保证数据的一致性. 当事务完成, 就会将完成的消息放入消息队列. 下游的系统进行消息的消费.
缺陷
- 业务逻辑复杂.
- 只能保证数据的最终一致性.
总结
在本篇文章中一共介绍了四种分布式事务一致性的解决方案, 并讨论了它们各自的优势和缺陷.
2PC
最基础的解决方案模型, 将事务的提交分为 Prepare 和 Commit 两个阶段, 通过协调者来保证所有参与者之间的状态一致. 但是由于没有超时机制, 当协调者宕机时可能出现参与者的无限等待, 无法释放资源等问题.
3PC
在 2PC 的基础上又增加了一个缓冲的阶段以减少参与者持有数据库锁的时间, 并且增加了超时机制以避免资源无法被释放的问题. 但是仍然没有完全解决数据不一致的问题.
TCC
对于每一个业务都提供 Try, Confirm 和 Cancel 三个接口给事务协调服务, 事务协调服务会保证数据的一致性. 这样做会导致代码的业务侵入严重, 但是同时保证了开发人员对于业务流程更精细化的处理.
异步
上面三种解决方案都是同步的解决方案, 并不适合高并发量的场景. 在异步解决方案中我们借助消息队列来连接上游和下游的业务. 通过一个可靠的消息管理服务来保证事务的一致性.