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).

Sequence diagram, time flowing down. The Coordinator writes BEGIN T1 and sends PREPARE to Orders, Payments and Stock. Each locks its row, force-writes PREPARED T1 and answers YES. The Coordinator force-writes COMMIT T1, the commit point, and sends COMMIT. Each participant force-writes COMMIT T1, releases its lock and sends ACK. The Coordinator writes END T1. Four message delays in all
Four message delays and seven forced log writes; T1 is committed the moment COMMIT T1 is on the coordinator's disk.

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

  1. Phase 1, voting. The coordinator writes BEGIN T1 with the list of participants and sends PREPARE. A participant does its work, keeps its locks, force-writes PREPARED T1 and votes YES. If it cannot do its part (Payments: insufficient funds) it aborts on its own, writes ABORT T1 and votes NO.
  2. Phase 2, the decision. Three YES votes: the coordinator force-writes COMMIT T1. A NO, or a vote that does not come: it force-writes ABORT T1. It sends the decision; each participant applies it, releases its locks and sends ACK. With every ACK in, the coordinator writes END T1 and 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.

Two state machines. Coordinator: INIT to WAIT on sending PREPARE; WAIT to COMMIT on three YES; WAIT to ABORT on a NO or the vote timeout; COMMIT or ABORT to END when every ACK is in. Participant: INIT to PREPARED when it votes YES; INIT to ABORTED when it votes NO or times out before voting; PREPARED to COMMITTED or ABORTED only on the coordinator's decision. The PREPARED state is marked: in doubt, no timeout edge leaves it
Every state has a way out on its own except PREPARED: once it has voted YES, a participant can only leave it when it hears the decision.
node, stateit waits forif it waits too long
coordinator, WAITthe votesabort: force ABORT T1 (vote timeout, Demo 3)
coordinator, COMMIT / ABORTthe ACKsresend the decision to the silent ones (Demo 6)
participant, INITPREPAREabort on its own; a later PREPARE gets NO
participant, PREPAREDthe decisionnothing 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:

recordwhoforced?why
BEGIN T1 {O, P, S}coordinatornoif it is lost, the coordinator knows nothing of T1 and the answer is abort, which is safe: no COMMIT was ever sent
PREPARED T1participantyes, before YESthe 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 T1coordinatoryes, before sendingthis record is the decision; a restarted coordinator must give the same answer it may already have given
COMMIT T1participantyes, before ACKafter all ACKs the coordinator forgets T1, so no one could tell the participant again
ABORT T1participantnolost, it leaves PREPARED or nothing; either way the answer is abort again
END T1coordinatornolost, 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.

A participant's time line: before its YES vote it may abort alone; from the YES until the decision arrives it is in doubt and holds its locks, marked in red; after the decision it commits or aborts and releases the locks. Below, the coordinator crashes inside the window, stretching it until the coordinator is back
The window between a YES vote and the decision is the price of atomicity: a coordinator crash there stretches it for as long as the coordinator is down.

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:

nodelast record for T1what it does
coordinatornone, or ENDnothing; if asked about T1, answers ABORT
coordinatorBEGIN onlyit never decided, and the votes are gone: force ABORT T1, send ABORT to all (Demo 4)
coordinatorCOMMIT or ABORTthe decision stands: resend it to all and wait for ACKs (Demo 5)
participantnoneT1 never prepared here; undo any unprepared work (Demo 3)
participantPREPAREDstill in doubt: take the locks again before serving anyone, then ask STATUS? (Demo 6)
participantCOMMIT or ABORTfinished; 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.