两阶段提交(2PC)原理分析
两阶段提交(2PC)原理分析
两阶段提交(2PC)是一种用于分布式系统中事务处理的算法,确保所有节点在提交事务时保持一致性。本文将详细介绍2PC的原理、流程、事务日志处理以及故障处理策略。
协议整体流程
上图展示了2PC的基本流程,其中Database A和Database C是两个参与者(worker/participant/cohort),通常是关系数据库。协调者(coordinator/transaction manager)一般位于应用服务器(如Java EE容器)中,也可以单独部署。2PC的执行过程可以分为三个阶段:
发送SQL语句阶段:在客户端开始分布式事务时,coordinator先生成一个事务ID,然后在每个worker上开启事务。客户端通过coordinator向每个worker发送多个读写SQL。在悲观串行化隔离级别下,worker需要用读锁和写锁分别锁定所有被读取和写入的数据。
准备(Prepare)阶段:coordinator分别给各个worker发送询问确认单节点内的事务是否可以保证被提交。如有任何一个节点返回no,则回滚(abort)所有节点的事务。
提交(Commit)阶段:coordinator给每个worker发送提交事务的指令,最终提交本地的事务并释放资源相关的锁。
事务日志处理
上图展示了事务日志的处理流程。为了确保事务的可靠性和故障恢复能力,coordinator和worker需要在关键进度点记录日志。具体来说:
coordinator的日志记录:
在prepare阶段开始时记录BEGIN日志,包含事务ID和涉及的worker。
prepare全部确认后记录COMMITTED日志。一旦记录COMMITTED日志,意味着这个事务一定会被提交。
worker的日志记录:
确认事务可以提交后记录BEGIN和PREPARED日志。例如,MySQL使用redo log来记录prepare。
在自己提交后记录COMMITTED日志。例如,MySQL使用binlog来记录COMMITTED日志。
故障点及处理
以下是可能的故障处理策略:
故障发生在发送SQL语句阶段:如果任何节点发生故障,可以直接给客户端返回错误。有些开始了事务的worker,coordinator可以将其回滚。如果coordinator挂了,则worker需要自己等待事务超时后回滚。
故障发生在prepare阶段:
情况1:worker发生了故障。coordinator需要不停地重试,直到worker恢复后给出响应。因为coordinator无法确认故障worker是否已经进入了prepared阶段,所以不能直接回滚整体事务。
情况2:coordinator发生了故障。coordinator在恢复后会从日志中读取到该已经BEGIN但未COMMITTED的事务,然后重新对每个worker发起prepare操作。worker需要保证prepare操作是幂等的。
故障发生在commit阶段:
情况1:worker发生了故障。coordinator需要重试,直到worker恢复后给出响应。因为worker在prepare阶段记录了事务细节并锁定了临界资源,所以能保证宕机恢复后事务依然一定可以提交成功。
情况2:coordinator发生了故障。coordinator在恢复后会从日志中读取到该已经COMMITTED但未被删除的事务记录,然后重新对每个worker发起commit操作。worker需要保证commit操作是幂等的。
2PC的问题
长久锁定的问题(blocking problem):在prepare之后每个worker需要继续锁定相关资源,一直到收到coordinator的commit指令。如果coordinator在prepare之后commit之前挂了,会导致临界资源在coordinator恢复前会一直处于锁定状态。
性能问题:多次远程通讯、多次日志写入、锁定数据等均会降低性能。据测试,MySQL中的分布式事务比单节点事务慢10倍以上,TPS参考值为几千。
部署问题:如果将coordinator与应用服务器部署到一起,因为coordinator需要存储日志,会将应用服务器变为有状态的服务,会降低其运维的灵活性。如果独立部署coordinator又会增加一层远程调用。
死锁问题:coordinator无法检测跨多个worker的死锁。
脏读的问题:2PC因提交阶段是无法保证所有worker都在同一个时间点commit的,所以先被commit的资源会先被读取到。
思考
为什么要prepare阶段?:prepare阶段worker会将事务进度持久化,从而保证worker宕机恢复后可以继续提交事务。prepare返回true代表worker承诺事务一定能提交。
有没有可能去掉prepare阶段呢?:是可能的。但需要支持commit之后的逆向操作(undone, compensating transaction),且在整体事务完成前要保持对资源的锁定,这导致最终还是需要一次确认操作来释放锁。这样跟加入prepare阶段比并不能带来优势。
参考资料
- http://dbmsmusings.blogspot.com/2019/01/its-time-to-move-on-from-two-phase.html
- https://www.youtube.com/watch?v=B6btpukqHpM
- Principles of Computer System Design An Introduction_Chapter 9_atomicity.pdf