PostgreSQL SELECT FOR UPDATE执行顺序与WHERE子句生效校验问题
关于带WHERE子句的
SELECT ... FOR UPDATE的核心问题解答 是否需要在业务服务层校验WHERE条件实际生效
必须校验,不能把数据库的锁机制当成业务逻辑的兜底。SELECT ... FOR UPDATE 只会按照SQL语句中明确写出的条件做匹配,不会感知你业务层的自定义规则:
- 引擎层的校验逻辑只会判断行数据是否满足你写的WHERE表达式,类似“数据归属是否为当前用户”“关联业务状态是否符合流转要求”这类业务规则,数据库完全无法识别
- 如果你的WHERE条件本身存在逻辑漏洞,数据库会严格按照你写的错误逻辑执行,不会主动报错
- 哪怕引擎自动过滤了锁等待期间不再满足条件的行,你也需要校验最终返回的结果集是否符合预期,避免出现“没查到目标行还继续往下执行业务更新”的问题。
你描述的执行顺序是否正确
你列的执行顺序存在核心错误,以InnoDB引擎为例,SELECT ... FOR UPDATE属于当前读,实际执行逻辑如下:
- 第一步:语句启动时,先基于当前读(不是事务启动时的一致性快照)定位所有初始满足WHERE条件的行
- 第二步:对定位到的行逐行申请排他锁,申请过程中如果某行已经被其他未提交事务持有锁,当前事务会进入锁等待状态
- 第三步:成功拿到某行的锁之后,引擎会重新读取这行的最新版本,再次校验是否满足WHERE条件:如果锁等待期间其他事务修改了行数据,导致该行已经不匹配WHERE规则,引擎会直接释放该行的锁,不会将其加入结果集;只有二次校验通过的行,才会最终返回给客户端。
补充:这个锁后二次校验的行为在RC(读提交)隔离级别下默认触发;RR(可重复读)隔离级别下,唯一索引等值查询场景因为Next-Key Lock会锁定对应索引记录和间隙,不会出现其他事务修改满足条件行的情况,不会触发二次校验,其余非唯一索引、范围查询场景依然会有相同逻辑。
执行行为的具体验证方法
你可以开两个MySQL客户端会话,通过并发场景手动复现验证,步骤如下:
- 先创建测试表并初始化测试数据
CREATE TABLE `test_for_update` ( `id` int NOT NULL AUTO_INCREMENT, `status` tinyint NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB; INSERT INTO test_for_update(status) VALUES (0);
- 两个会话都设置隔离级别为RC,方便复现核心逻辑
-- 两个会话分别执行 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 会话2先开启事务,执行带WHERE条件的FOR UPDATE语句,持有目标行的锁
-- 会话2执行 BEGIN; SELECT * FROM test_for_update WHERE status = 0 FOR UPDATE; -- 此时会正常返回id=1、status=0的行,锁持有成功
- 会话1开启事务,执行相同的FOR UPDATE语句,此时会因为会话2持有锁进入等待状态
-- 会话1执行,执行后会卡住等待锁 BEGIN; SELECT * FROM test_for_update WHERE status = 0 FOR UPDATE;
- 回到会话2,将该行的status修改为不满足WHERE条件的值,然后提交事务释放锁
-- 会话2执行 UPDATE test_for_update SET status = 1 WHERE id = 1; COMMIT;
- 观察会话1的返回结果:会发现会话1没有返回任何行,哪怕它一开始进入锁等待时,该行是满足status=0的条件的,这就验证了“拿锁后二次校验WHERE条件,不匹配则不返回”的逻辑。
内容的提问来源于stack exchange,提问作者Daniil Pleshkov
相关产品推荐
相关产品推荐

