Percolator 用“主锁 + 次锁”让一个事务跨多行(多个分片)原子提交,不需要独立的协调者进程。它怎么提交?客户端崩溃后谁来收尾?读到别人的锁怎么办?这篇自己实现一个最小版本(单机 MySQL 模拟存储),用 kill -9 和压力测试把这些路径逐个跑出来。
初始(已提交):
data(k, ts, v):[('A', 101, 100), ('B', 101, 0)]
lock(k, ts, 主锁):[]
write(k, commit_ts, start_ts):[('A', 102, 101), ('B', 102, 101)]
预写之后(start_ts=103,主锁 = A,B 上的锁指向 A):
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[('A', 103, 'A'), ('B', 103, 'A')]
write(k, commit_ts, start_ts):[('A', 102, 101), ('B', 102, 101)]
提交主锁 A 之后(commit_ts=104):A 有了提交记录、锁没了;B 还有锁 —— 此刻这个事务已经算提交了:
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[('B', 103, 'A')]
write(k, commit_ts, start_ts):[('A', 102, 101), ('A', 104, 103), ('B', 102, 101)]
提交次锁 B 之后(锁全部清掉):
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[]
write(k, commit_ts, start_ts):[('A', 102, 101), ('A', 104, 103), ('B', 102, 101), ('B', 104, 103)]
新事务(start_ts=105)读到 A = 50,B = 50预写时每个 key 写 data 和 lock,次锁记着主锁在哪;提交主锁的那一次本地事务就是整个事务的提交点。
T1(start_ts=103)读 x = 1(T2 在 commit_ts=105 提交了 x = 2,T1 的快照看不到);新事务读 x = 2 T1 提交:预写失败 —— x:有比 start_ts 更新的提交记录(写写冲突);锁 / 数据都已清理:lock 行数 0
读只看 start_ts 之前提交的版本;预写时发现更新的提交记录或别人的锁,就中止(先提交者赢)。
kill -9 之后:
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[('A', 103, 'A'), ('B', 103, 'A')]
write(k, commit_ts, start_ts):[('A', 102, 101), ('B', 102, 101)]
新事务读 B:等了 1486 ms(锁的 TTL 是 1500 ms,过期之前主锁还可能属于一个正在进行的事务,只能等)才读到 B = 0;等待轮询 107 次,回滚了 1 把锁
再读 A = 100(A 的锁在回滚 B 时已经一并清掉);lock 行数 0,data 行 [('A', 101, 100), ('B', 101, 0)]锁遗留在库里,碰到的人看主锁:未过期就等,过期就回滚整个事务。
kill -9 之后(A 已提交、B 的锁还在):
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[('B', 103, 'A')]
write(k, commit_ts, start_ts):[('A', 102, 101), ('A', 104, 103), ('B', 102, 101)]
只看提交记录、不管锁的读:A = 50,B = 0,合计 50(应该是 100)—— 看到了转账的一半
会处理锁的读:B = 50(用时 0.7 ms,没有等待:前滚了 1 把锁),A = 50,合计 100
读之后:B 的锁被换成了提交记录:
data(k, ts, v):[('A', 101, 100), ('A', 103, 50), ('B', 101, 0), ('B', 103, 50)]
lock(k, ts, 主锁):[]
write(k, commit_ts, start_ts):[('A', 102, 101), ('A', 104, 103), ('B', 102, 101), ('B', 104, 103)]事务已经提交,碰到遗留次锁的人看一眼主锁就前滚,不用等。
600 次转账用时 4.0 s:{'committed': 310, 'conflict': 260, 'crash_pre': 15, 'crash_primary': 15};读线程处理锁的次数:前滚 69、回滚 34、等待轮询 2016
会处理锁的快照读:215 次检查,总额 ≠ 1000 的 0 次;不管锁的读:3339 次检查,总额 ≠ 1000 的 11 次
全部处理完之后的最终快照:[12, 88, -30, 30, 408, 29, 10, 209, 44, 200],合计 1000;lock 行数 0每个读都先处理遇到的锁,快照读的总额始终正确;只看提交记录不管锁的读会看到半个事务。