两阶段提交(2PC)把“多个库一起提交”拆成投票和执行两步。它到底保证了什么、prepare 之后事务处在什么状态、代价是多少?这篇用真实的 PostgreSQL PREPARE TRANSACTION 把每一步都跑出来,并量出它在延迟、fsync 次数和热点行吞吐上的账。
初始:订单库 0 条订单,库存库 sku-1=100
订单库 BEGIN; INSERT INTO orders ...
库存库 BEGIN; UPDATE stock SET qty = qty - 1 ...
订单库 PREPARE TRANSACTION 'tx-1' -> PREPARE TRANSACTION
库存库 PREPARE TRANSACTION 'tx-1' -> PREPARE TRANSACTION
订单库 pg_prepared_xacts: [('tx-1', 'postgres', 'postgres')]
库存库 pg_prepared_xacts: [('tx-1', 'postgres', 'postgres')]
prepare 之后别的会话看到:订单库 0 条订单,库存库 sku-1=100 (两个修改都还不可见)
订单库 COMMIT PREPARED 'tx-1' -> COMMIT PREPARED
库存库 COMMIT PREPARED 'tx-1' -> COMMIT PREPARED
提交后:订单库 1 条订单,库存库 sku-1=99;pg_prepared_xacts 里剩 0 / 0阶段一每个参与者执行修改后 PREPARE,把“我能提交”的承诺写进日志;全部 YES 后,协调者在阶段二让大家 COMMIT PREPARED。prepare 之后修改不可见,提交后才可见。
协调者的连接已关闭。pg_prepared_xacts: [('tx-2', 'postgres')]
pg_locks 里属于它的锁(pid 为空 = 没有会话持有):
('relation', 'RowExclusiveLock', True, True, 'stock')
('relation', 'RowExclusiveLock', True, True, 'stock_pkey')
('transactionid', 'ExclusiveLock', True, True, '-')
别的会话 SELECT:读到 qty=100(旧值),用时 4.3 ms,没有被挡
别的会话 UPDATE 同一行(lock_timeout=1500ms):LockNotAvailable canceling statement due to lock timeout,等了 1502.0 ms
另一个会话的 UPDATE 阻塞着,1.2 秒后协调者 COMMIT PREPARED 'tx-2':它等了 1206.4 ms 后返回;最终 qty=94(100 - 1 - 5)协调者的连接断了,prepared 事务和它的行锁仍在。读(MVCC)不被挡,写同一行被挡到有人提交或回滚。
kill -9 postmaster 和全部子进程
重启用时 129.4 ms
重启后 pg_prepared_xacts: [('tx-3',)]
重启后别的会话 UPDATE 同一行:LockNotAvailable canceling statement due to lock timeout(行锁被恢复了)
COMMIT PREPARED 'tx-3' -> COMMIT PREPARED
提交后 qty=99
PostgreSQL 日志里和恢复有关的行:
LOG: database system was not properly shut down; automatic recovery in progress
LOG: recovering prepared transaction 739 from shared memory
LOG: checkpoint starting: end-of-recovery immediate wait
LOG: 1 two-phase state file was written for a long-running prepared transaction参与者在 PREPARE 时把恢复所需信息写进 WAL。整个实例 kill -9 重启后,prepared 事务和它的锁都被恢复。它承诺过的事,不会因为崩溃而反悔。
一个实例里一个事务:中位数 0.2 ms,p99 0.5 ms;两个实例合计 WAL fsync 300 次 = 每个事务 1.00 次 两个实例做 2PC: 中位数 1.1 ms,p99 3.0 ms;两个实例合计 WAL fsync 1200 次 = 每个事务 4.00 次 2PC / 单实例:中位数 6.9 倍;fsync 4.0 倍(协调者自己写决定日志的 fsync 不在这里面,它写在 SQLite 里)
两个参与者各一次 prepare、一次 commit prepared,每个事务 4 次 WAL fsync,是单实例的 4 倍;提交延迟中位数 6.9 倍。协调者的决定日志还要再算一次落盘。
prepare 之后多等 | 两线程抢同一行 (事务/秒) | 估算 1000/(hold+3.3 ms) | 两线程各用各的行 (事务/秒) | 各自的行 / 同一行 -------------+----------------+-----------------------+-----------------+----------- 0 ms | 307 | 307 | 1094 | 3.6 50 ms | 18 | 19 | 35 | 2.0 200 ms | 5 | 5 | 10 | 2.0 同一行上的事务是一个接一个排队的:每个事务占着行锁的时间 = 2PC 自己的耗时 + hold;不同行的事务互不影响,仍然能并行
行锁从 prepare 一直握到 commit prepared。同一行上的事务只能排队,吞吐约等于 1000/(持锁时间)。协调者慢一点,热点行的吞吐就按比例塌下去。