单行更新依赖同表多行状态时如何实现并发控制?
问题本质
这个场景是典型的读-校验-写间隙并发冲突:校验逻辑依赖的关联行数据在查询完成后、目标行更新前被其他事务修改,普通快照读不会对读集合加并发保护,最终导致校验结果失效、错误更新目标行。你提到的“更新所有关联行实现加锁”本质是靠更新操作隐式触发行锁,但是副作用太多,完全有更轻量可靠的方案。
推荐实现方案
按优先级从高到低排列:
1. 原子条件更新(性能最优,优先选择)
把校验逻辑直接合并到目标行的UPDATE语句中,利用数据库单条SQL执行的原子性,完全消除读和写之间的时间窗口,不需要额外加显式锁,甚至不需要开长事务。
举个例子,假设业务规则是「id=123的目标行,只有相邻id=122、124的关联行状态均为VALID时,才能更新为ACTIVE」,直接写单条SQL即可:
UPDATE biz_table SET status = 'ACTIVE' WHERE id = 123 AND (SELECT status FROM biz_table WHERE id = 122) = 'VALID' AND (SELECT status FROM biz_table WHERE id = 124) = 'VALID';
执行后直接判断影响行数:
- 返回1:说明校验通过,更新成功
- 返回0:说明关联行状态已经不符合规则,更新被拦截,直接返回业务错误即可
这个方案没有任何额外锁开销,也不会产生无意义的数据修改,是这类场景的首选。
2. 事务内加当前读锁(兼容复杂校验逻辑)
如果你的校验逻辑太复杂,没法直接写到单条UPDATE语句里,就在同一个事务中查询关联行时,用SELECT ... FOR UPDATE加排他行锁,直接锁定所有校验依赖的关联行+目标行,事务提交前其他事务无法修改这些行,从根源避免校验过程中数据被篡改。
示例代码:
BEGIN; -- 注意:查询时按固定顺序(比如id升序)查所有需要的行,加排他锁,避免死锁 SELECT id, status FROM biz_table WHERE id IN (122,123,124) ORDER BY id FOR UPDATE; -- 这里可以承载任意复杂度的业务校验逻辑,锁持有期间关联行不会被修改 -- 校验通过后再更新目标行 UPDATE biz_table SET status = 'ACTIVE' WHERE id = 123; COMMIT;
注意事项:
- 查询条件必须命中索引,否则会退化为表锁,性能极差
- 所有访问这些行的事务都要按相同顺序加锁,否则容易出现死锁
- 不要在锁持有期间加RPC调用、长耗时计算,尽量缩短事务长度
3. 乐观锁(适合低并发场景)
如果你的业务并发量不高,不想加悲观锁影响吞吐量,可以给表增加全局version字段,每次查询关联行时把所有关联行的version值都记录下来,更新目标行时校验所有关联行的version是否和查询时一致,一致才提交更新,否则回滚重试。
这个方案的缺点是冲突重试的成本高,高并发下重试太多会拖垮性能,只适合并发量低的内部系统使用。
不推荐“更新关联行加锁”的原因
这个方案虽然能实现加锁效果,但是副作用非常明显:
- 会产生大量无意义的binlog、redo log,增加数据库存储、主从同步的压力
- 会错误更新关联行的自动填充字段(比如
update_time、updater),甚至触发关联行上的更新触发器、缓存失效、业务回调,产生意料之外的脏数据和逻辑错误 - 锁粒度和
SELECT ... FOR UPDATE完全一致,但是额外多了一次无意义的写操作,性能更差
内容的提问来源于stack exchange,提问作者pilotnik
相关产品推荐
相关产品推荐

