第 14 章:用两条连接重放快照与等待
同一事务里读到不同数值,可能是隔离级别改变,也可能只是读取方式不同。实验先控制两条连接的交错顺序,再让数据库返回结果。只有把“谁先开始、谁先提交、哪一条建立快照”写清楚,数字才具有解释力。
14.1 重放 RR 的 2、2、3
可以单独运行快照场景:
python3 -m unittest \
lab.scenarios.InnoDBCases.test_03_rr_snapshot_starts_at_first_consistent_read -v测试在 A 执行 BEGIN 后,让 B 把库存改为 2 并提交,再让 A 第一次普通读取。B 随后改为 3 并提交;A 第二次读取,结束事务后再读。真实记录是:
{"after_begin_before_first_read":2,
"same_snapshot":2,"new_transaction":3}第一次读到 2,说明本例读视图没有在 BEGIN 就固定;第二次仍为 2,说明后续普通一致性读沿用它;新事务读到 3,说明后来提交的数据确实存在。三个数要一起检查。只展示中间的两个 2,不能排除 B 根本没有成功更新。
做改参实验时,可以在 A 的 BEGIN 后、B 提交前插入一次普通库存查询,再预测后续结果。这个新增读取会提前建立快照,原来的 2、2、3 断言就需要重新推导。它是待你独立运行的扩展,不是现有十五项结果的一部分。
14.2 RC 与锁定读分别改变什么
RC 场景在同一事务里先读 1,另一个连接提交 2,再读得到 2。完整证据记录 first=1, second=2。这里改变的是一致性读的快照策略,不代表数据库忽略了写锁,也没有解决所有先读后写的竞争。
另一个 RR 场景保留了更容易误解的对照:
{"consistent_before":1,"locking_read":2,"consistent_after":1}A 先普通读取建立旧视图;B 更新并提交;A 的 FOR UPDATE 读取较新的记录并加锁,再做普通读取仍使用旧视图。本例 A 没有修改这条记录。若你在锁定读后加入自己的 UPDATE,后续普通读还会受到“能够看见自身修改”的规则影响,不能继续机械地套用最后那个 1。
建议先分别运行场景 04 与 05,再回看源码中的每条语句。把所有 SELECT 都换成 FOR UPDATE,可能让某个数字更容易解释,却同时改变了锁与等待成本,也丢失了原实验要观察的区别。
14.3 在竞争发生时取证
等待实验先让 holder 更新账户 1 且不提交,再启动第二条连接修改同一行。独立观察连接轮询 Performance Schema,筛选当前随机数据库的 accounts 表。此次记录为:
{"observed_lock_wait_edges":1,
"blocker_rollback_released_waiter":true}断言要求至少观察到一条实际等待边,不要求所有环境永远恰好只有一条。随后 holder 回滚,等待者完成并提交,最终余额成为 101。等待边与最终状态相互补充,证明程序不只是让一个线程睡眠来伪装锁等待。
python3 -m unittest \
lab.scenarios.InnoDBCases.test_15_performance_schema_exposes_real_blocking_edge -v若在等待者完成以后才查询锁表,返回零可能只是竞争窗口已经结束。不要据此断言数据库从未等待。扩大观察窗口的改参方式是延长受控持锁阶段,同时保持等待者超时、线程 join 和清理期限相容;不能把 join 改成无限等待来掩盖阻塞。
这里的线程事件仅说明等待者已经准备进入 SQL,不等于锁等待必然已经建立。真正使结论成立的是随后在 Performance Schema 中读到数据库等待边。轮询与线程退出都有期限:较慢环境若没捕获到窗口,应检查调度、权限、连接错误和时间安排,而不能把“没看见”改写成“已经证明没有等待”。
同一测试中的观察连接选择了本次随机库,查询也带库名与表名过滤。把这些条件拿掉,可能把别的会话造成的等待当成自己的成功证据。分析行锁时还要区分持锁语句和阻塞者当前语句,两者不必相同。
14.4 计划是另一种证据
索引场景在 2000 行 people 上比较同一条查询。增加唯一 email_idx 后,JSON 计划从 ALL、估计扫描 2000 行,变为本次 const、估计扫描 1 行。它证明访问路径改变;没有测出响应时间提升几倍,更不能把估计扫描量当作物理磁盘读取次数。
这也提示我们,锁等待与扫描问题需要不同证据。执行计划不能单独证明没有阻塞,等待边也不能解释所有查询成本。实际排查应先确认观察时刻与访问模式,再决定补哪一种信息。
练习。 为 RR、RC 和锁定读各画一条时间线,明确首次一致性读与另一个事务提交的位置。然后解释:为什么一次锁表空结果,和一次 EXPLAIN 使用索引,都不能单独证明用户请求没有发生数据库等待?
快照、锁定读与计划的官方入口见资料附录。