第 5 / 22 篇

第 4 章:普通读取与锁定读取回答不同的问题

普通 SELECT 适合读取一致性视图,SELECT ... FOR UPDATE 则用于在事务中锁定将要处理的记录。两者混用时,如果还假设整个事务只有同一个可见版本,就容易错误解释结果。

4.1 一个看似矛盾的结果

RR 实验先读库存得到 1;另一个事务改为 2 并提交。原事务执行 FOR UPDATE 读到 2,再次做普通 SELECT 仍读到 1。三个结果分别来自一致性读、锁定读和原来的读视图。

SELECT stock FROM inventory WHERE sku=1;
SELECT stock FROM inventory WHERE sku=1 FOR UPDATE;

这不是数据库把一次 SELECT 缓存在客户端。它说明读取模式是语句语义的一部分。更新语句也不能简单按普通 SELECT 的历史快照来推导。

4.2 锁需要覆盖后续的业务动作

如果采用先读后改,应在显式事务里进行锁定读,并在同一事务中检查和修改。若每条语句都自动提交,锁无法按预期保护后续业务阶段。

START TRANSACTION;
SELECT stock FROM inventory WHERE sku=1 FOR UPDATE;
-- 在持有记录锁期间检查并修改
COMMIT;

持锁期间不宜等待用户输入或长时间调用外部服务。数据库锁只保护数据库中的竞争,还会占用其他事务的等待时间;网络操作应尽量移到合适的事务边界之外。

4.3 SKIP LOCKED 适用于队列式认领

两个工作者从本地事件表取任务,可以使用 FOR UPDATE SKIP LOCKED 让第二个工作者跳过第一个已锁住的记录。本实验里,工作者 A 取得事件 1,B 取得事件 2;A 回滚后,另一个工作者又能够取得事件 1。

这证明锁与回滚的本地认领行为,不证明消息已经发送一次。SKIP LOCKED 会跳过数据,得到的不是一般事务所需的一致完整视图,官方手册也明确提示其适用边界。不能将它直接用于要求完整结果的财务统计。

4.4 锁定范围由访问路径参与决定

唯一键等值查找、非唯一范围扫描与全表扫描,可能涉及不同的索引记录和间隙。不能只看 SQL 写了一个业务条件,就认定只锁定自己期望的那一行。

本书会用执行计划与实际等待关系一起排查。只看“有索引”不足以判断锁范围,必须确认使用了哪个索引、条件是否形成范围,以及使用什么隔离级别。

练习。 为什么事件表工作者适合考虑 SKIP LOCKED,而“计算今天所有应付款总额”通常不适合?请分别写出对跳过记录的容忍程度,以及跳过后必须怎样继续处理。