Three-phase commit (3PC) is a protocol designed to improve upon two-phase commit (2PC) by adding an extra phase to make it more resilient to coordinator failures.
Let’s see how it works in practice. Imagine a distributed transaction involving three services: OrderService, InventoryService, and PaymentService. The coordinator, let’s say TransactionManager, orchestrates the commit.
Phase 1: CanCommit
The TransactionManager sends a CanCommit message to all participants.
TransactionManager -> OrderService: CanCommit(tx_id=123)
TransactionManager -> InventoryService: CanCommit(tx_id=123)
TransactionManager -> PaymentService: CanCommit(tx_id=123)
Each participant checks if it can perform its part of the transaction. If it can, it responds with Yes. If not, it responds with No and aborts its local transaction.
OrderService -> TransactionManager: Yes
InventoryService -> TransactionManager: Yes
PaymentService -> TransactionManager: Yes
Phase 2: PreCommit
If all participants respond Yes, the TransactionManager sends a PreCommit message. This is the crucial addition that distinguishes 3PC from 2PC. The PreCommit message signals that the transaction is likely to commit, but not yet guaranteed.
TransactionManager -> OrderService: PreCommit(tx_id=123)
TransactionManager -> InventoryService: PreCommit(tx_id=123)
TransactionManager -> PaymentService: PreCommit(tx_id=123)
Upon receiving PreCommit, participants prepare their resources to commit by writing the transaction to a durable log (e.g., a transaction log, WAL). They do not yet commit the transaction itself. This durable log entry ensures that even if the participant crashes, it knows the transaction was in a pre-committed state.
OrderService -> TransactionManager: Ack
InventoryService -> TransactionManager: Ack
PaymentService -> TransactionManager: Ack
Phase 3: DoCommit
Once the TransactionManager receives acknowledgments from all participants for PreCommit, it sends the final DoCommit message.
TransactionManager -> OrderService: DoCommit(tx_id=123)
TransactionManager -> InventoryService: DoCommit(tx_id=123)
TransactionManager -> PaymentService: DoCommit(tx_id=123)
Participants, upon receiving DoCommit, finalize the transaction by applying the changes and releasing any locks. They then send a final Ack to the coordinator.
OrderService -> TransactionManager: Ack
InventoryService -> TransactionManager: Ack
PaymentService -> TransactionManager: Ack
The problem 3PC solves is the blocking nature of 2PC during coordinator failure. In 2PC, if the coordinator fails after sending Prepare but before sending Commit or Abort, participants are left in an uncertain state. They don’t know whether to commit or abort and must wait for the coordinator to recover. 3PC introduces the PreCommit phase to mitigate this. If the coordinator fails after participants have acknowledged PreCommit (meaning they’ve durably logged their intention to commit), the system can still recover. If the coordinator fails before sending PreCommit, participants can time out and unilaterally abort.
The mental model for 3PC is that it adds a "prepared to commit" state. This state is durable on the participants’ side. If the coordinator disappears, participants in this state know that the transaction is intended to commit, and they can eventually proceed to commit once a recovery mechanism is in place, or if a new coordinator takes over. The CanCommit phase is essentially the Prepare phase of 2PC. The PreCommit phase is the new addition, establishing a common point of knowledge about the transaction’s intent to commit. The DoCommit phase is the finalization step.
A surprising aspect of 3PC is its inability to completely eliminate blocking in the presence of network partitions and coordinator failure simultaneously. If a coordinator sends PreCommit to some participants but then fails, and a network partition prevents other participants from receiving PreCommit or the coordinator from reaching them, those participants might time out and abort, while others might eventually commit. This leads to an inconsistent state. The protocol is often described as non-blocking, but this only holds true if the coordinator is the only point of failure and there are no network partitions.
The core idea is that a participant, after receiving PreCommit and logging it, is guaranteed to eventually commit. If the coordinator fails, other participants can eventually reach a consensus to commit based on the durable PreCommit logs. The extra phase provides a window for participants to acknowledge their readiness to commit before the final commit decision is broadcast, allowing for recovery mechanisms to function more effectively.
The next concept to explore is how recovery managers or backup coordinators are implemented to handle coordinator failures in a real-world 3PC system.