无显式悲观行锁执行UPDATE扣库存在MySQL/PG/Oracle是否安全
核心问询
未添加显式悲观行锁时,执行UPDATE t SET v=v-1 WHERE id= ? and v>0这类库存扣减语句,在MySQL、PostgreSQL、Oracle三类数据库中是否真的安全?
场景概述
假设有一张用于管控库存数量的业务表,表结构与初始数据如下:
CREATE TABLE products( id INTEGER PRIMARY KEY, remaining_amount INTEGER NOT NULL ); INSERT INTO products(id, remaining_amount) VALUES (1, 1);
当前用户A与用户B同时尝试抢占最后1件库存,二者执行的UPDATE语句完全一致:
UPDATE products SET remaining_amount = remaining_amount - 1 WHERE id = 1 and remaining_amount > 0;
待明确的核心问题如下:
- 上述写法能否保证
remaining_amount字段永远不会出现负值?是否需要额外添加显式悲观行锁? - 该场景下应当选择哪种事务隔离级别:
READ COMMITTED、REPEATABLE READ、SERIALIZABLE,还是仅MySQL支持的READ UNCOMMITTED? - 不同RDBMS下,该问题的结论是否存在差异?
解答结论
不同数据库的行为差异
- MySQL(InnoDB引擎):不加显式悲观锁存在超扣风险。在READ COMMITTED、REPEATABLE READ两个常用隔离级别下,可能出现两个并发事务同时通过
remaining_amount > 0的条件判断,最终将库存扣为负值。必须在事务内先执行SELECT ... FOR UPDATE锁定目标行再更新,才能保证扣减逻辑正确。 - Oracle:无需额外添加显式悲观锁。Oracle内置写一致性机制:若UPDATE执行时发现目标行已被其他未提交事务修改,会自动回滚当前语句的执行,加上隐式行锁后重新读取最新行版本,再做条件判断与更新,从底层机制上避免了超扣问题。
- PostgreSQL:无需额外添加显式悲观锁。PG采用多版本不可变行存储结构,行被修改后旧版本会被标记为dead tuple,后续更新操作必须读取到最新已提交的行版本才能执行修改,
remaining_amount > 0的判断始终基于最新值,不会出现错误扣减。
隔离级别选型建议
- 直接排除
READ UNCOMMITTED,该级别会读取未提交的脏数据,完全无法保障数据一致性。 - Oracle、PostgreSQL场景使用默认的
READ COMMITTED隔离级别即可,无需升级到REPEATABLE READ或SERIALIZABLE。单条UPDATE的原子性配合数据库自身的实现机制,已经能保证扣减逻辑正确,更高隔离级别只会带来不必要的性能损耗。 - MySQL InnoDB场景下,无论使用READ COMMITTED还是REPEATABLE READ隔离级别,只要配合
SELECT ... FOR UPDATE显式加行锁即可,不需要开启SERIALIZABLE级别,性能性价比极低。
常见误区提醒
“单条SQL是原子操作就一定不会超扣”的结论不具备普适性,仅在Oracle、PostgreSQL的现有实现下成立。InnoDB的加锁逻辑与前两者存在本质差异,直接套用经验很容易出现线上超扣事故。
内容的提问来源于stack exchange,提问作者mpyw
相关产品推荐
相关产品推荐

