The idea: one decision for several databases
Transaction T1 is one order that touches three databases: Orders DB inserts order#7, Payments DB debits acct#12 and Stock DB takes one item#42. Each database can commit or roll back its own part, but T1 must happen everywhere or nowhere. Two-phase commit (2PC) gets that by putting one node, the coordinator (transaction manager), in charge of a single decision. The databases are the participants (resource managers).
On the canvas, every message takes one tick. A tick has two steps: the messages in flight arrive, then the replies appear at their senders. Each node shows its state, its lock and its log on disk; orange log records are forced (written with fsync before the node sends its next message). The timeline at the bottom keeps each node's state for every tick, and T2, another order for item#42, shows what T1's locks cost everyone else.
The two phases
- Phase 1, voting. The coordinator writes
BEGIN T1with the list of participants and sendsPREPARE. A participant does its work, keeps its locks, force-writesPREPARED T1and votesYES. If it cannot do its part (Payments: insufficient funds) it aborts on its own, writesABORT T1and votesNO. - Phase 2, the decision. Three YES votes: the coordinator force-writes
COMMIT T1. A NO, or a vote that does not come: it force-writesABORT T1. It sends the decision; each participant applies it, releases its locks and sendsACK. With every ACK in, the coordinator writesEND T1and forgets T1.
In the happy path that is 4 message delays and 12 messages (Demo 1). One NO is enough to abort: the coordinator sends ABORT only to the YES voters, because the NO voter has already aborted (Demo 2, 10 messages).
States: who may still decide alone
The key to 2PC is the moment a participant votes YES. Before it, the participant may still abort for any reason. After it, it has promised to commit if told to, even after a crash, so it can only wait for the coordinator.
| node, state | it waits for | if it waits too long |
|---|---|---|
| coordinator, WAIT | the votes | abort: force ABORT T1 (vote timeout, Demo 3) |
| coordinator, COMMIT / ABORT | the ACKs | resend the decision to the silent ones (Demo 6) |
| participant, INIT | PREPARE | abort on its own; a later PREPARE gets NO |
| participant, PREPARED | the decision | nothing it may decide: it asks STATUS? and keeps waiting, locks held |
The logs: what is forced and why
Each node writes a record to its log at every turn, and some of them must be on disk before the next message leaves. Each forced write costs an fsync (a few milliseconds on a disk, much less with group commit), so 2PC forces only where a crash would otherwise break a promise:
| record | who | forced? | why |
|---|---|---|---|
BEGIN T1 {O, P, S} | coordinator | no | if it is lost, the coordinator knows nothing of T1 and the answer is abort, which is safe: no COMMIT was ever sent |
PREPARED T1 | participant | yes, before YES | the YES is a promise; after a crash the participant must still be able to commit, so its changes and the fact that it voted must survive |
COMMIT T1 / ABORT T1 | coordinator | yes, before sending | this record is the decision; a restarted coordinator must give the same answer it may already have given |
COMMIT T1 | participant | yes, before ACK | after all ACKs the coordinator forgets T1, so no one could tell the participant again |
ABORT T1 | participant | no | lost, it leaves PREPARED or nothing; either way the answer is abort again |
END T1 | coordinator | no | lost, the coordinator just resends the decision once more after a restart |
So a commit costs 1 + 2 × (number of participants) forced writes: 7 in Demo 1.
Blocking: the in-doubt window
A participant that voted YES and has not heard the decision is in doubt. It cannot commit, because someone may have voted NO. It cannot abort, because the coordinator may already have written COMMIT and told another participant. So it keeps its locks and asks. If the coordinator is down, it waits as long as the coordinator is down: 2PC is a blocking protocol. In Demo 4 the coordinator crashes right after sending PREPARE: the votes are lost, all three databases sit in doubt for 5 ticks with their rows locked, and T2 waits all that time for item#42.
Asking the other participants helps only sometimes (the cooperative termination protocol): if any of them already got the decision, or has not voted yet (so it can still vote NO and abort), the in-doubt one learns the outcome. If all of them voted YES and none heard the decision, they are all stuck. Removing that last case needs more than two phases. Three-phase commit adds a "pre-commit" round, but it is only safe if messages are never delayed past a timeout, which real networks do not promise. The modern answer is to make the coordinator itself fault tolerant: Paxos Commit, or a coordinator whose log is replicated with Paxos or Raft, as in Spanner and CockroachDB.
Recovery from the log
A crash wipes a node's memory (votes, ACKs, the lock table, timers) but not its log. A restarted node reads its log and acts on the last record it finds for T1:
| node | last record for T1 | what it does |
|---|---|---|
| coordinator | none, or END | nothing; if asked about T1, answers ABORT |
| coordinator | BEGIN only | it never decided, and the votes are gone: force ABORT T1, send ABORT to all (Demo 4) |
| coordinator | COMMIT or ABORT | the decision stands: resend it to all and wait for ACKs (Demo 5) |
| participant | none | T1 never prepared here; undo any unprepared work (Demo 3) |
| participant | PREPARED | still in doubt: take the locks again before serving anyone, then ask STATUS? (Demo 6) |
| participant | COMMIT or ABORT | finished; ACK again if the decision comes again |
Demo 5 shows where the commit point is. The coordinator writes COMMIT T1 and crashes before sending anything. No participant has committed, yet T1 is committed: the restarted coordinator reads its log and sends COMMIT, never ABORT. In Demo 6, Stock crashes after its YES; its COMMIT is lost, and on restart it locks item#42 again from its PREPARED record, so T2 still cannot slip in before T1 is finished.
Presumed abort and other shortcuts
The page runs basic 2PC. Real systems cut messages and forced writes:
- Presumed abort (R*, the default in most XA implementations): "no record" means abort. The coordinator need not force
ABORT, participants need not ACK an abort, and the coordinator can forget an aborted transaction at once. Commits cost the same; aborts get cheaper. - Presumed commit: the reverse, cheaper commits, but the coordinator must force a record listing the participants before sending PREPARE.
- Read-only participants vote "read-only", release their locks at once and skip phase 2.
- One-phase commit: with a single participant there is nothing to agree on; the coordinator just asks it to commit.
Where you meet it
The XA standard is 2PC between a transaction manager and databases or brokers: in MySQL XA START, XA END, XA PREPARE, XA COMMIT and XA RECOVER (it lists the in-doubt transactions after a crash); in PostgreSQL PREPARE TRANSACTION 't1', COMMIT PREPARED 't1' and the view pg_prepared_xacts, with max_prepared_transactions set above 0. Java's JTA drives XA from an application server. A forgotten prepared transaction holds its locks forever, which is why both databases let an administrator resolve it by hand (a heuristic decision). Inside one MySQL server, 2PC also keeps InnoDB and the binlog in step (MySQL Binlog Group Commit). Kafka transactions commit across partitions with a coordinator and commit markers in the same two-phase shape.
See also Saga vs Two-Phase Commit for the alternative without distributed locks, Two-Phase Locking for the locks each participant holds until the decision (a different "two-phase"), Paxos and Raft for agreement that survives a crashed leader, and Distributed Lock with Redis for another way a node can believe it holds something it no longer does.
MongoDB commits a transaction that wrote to several shards the same way: a coordinator on one shard sends prepareTransaction, then commits everyone at the largest prepare timestamp (MongoDB Multi-Document Transactions).
What the page leaves out
The execution phase before PREPARE (the SQL that runs while the locks are taken), presumed abort and presumed commit on the canvas, the cooperative termination protocol, read-only and one-phase optimizations, several transactions at once and deadlocks between them, a lost PREPARE being resent, a participant that times out before voting, heuristic decisions and the damage they can do, network partitions (a node that is up but cannot be reached looks the same as a crashed one here), group commit of the forced writes, and a replicated coordinator.