不能先查再更新,因为存在不可控时间窗口导致并发冲突;正确做法是用单条UPDATE+WHERE原子校验,并检查SQL%ROWCOUNT判断是否成功。

直接用 UPDATE ... WHERE version = ? + 检查影响行数,别在存储过程里先 SELECT 再 UPDATE,否则必然超扣。
为什么不能先查再更新
Oracle 里写个 SELECT ... INTO 拿到当前 version 或 stock,再拼 UPDATE,看着逻辑清晰,但中间存在不可控时间窗口:两个会话几乎同时查到 version = 5、stock = 10,都判断“可扣”,结果双双执行 UPDATE,库存或版本号就乱了。
- 这个窗口无法靠事务隔离级别堵住——
READ COMMITTED下照样发生 -
SELECT FOR UPDATE虽能锁住,但属于悲观锁,高并发下容易阻塞、升级锁、拖慢整体吞吐 - Oracle 的行级锁机制不保护“读-改-写”这种非原子操作,只保护单条 DML 的原子性
正确写法:一条 UPDATE 带条件校验
把判断逻辑全塞进 WHERE 子句,让 Oracle 在引擎层原子执行。典型结构:
UPDATE products
SET stock = stock - 1,
version = version + 1,
updated_at = SYSDATE
WHERE id = :p_id
AND version = :p_expected_version
AND stock >= 1;执行后立刻检查 SQL%ROWCOUNT(PL/SQL 中):
- 等于 1 → 成功扣减,继续后续逻辑(如记录日志、发消息)
- 等于 0 → 失败:可能是版本已变、库存不足、或记录被删;业务需按此分支返回明确错误(如“库存已变更,请刷新重试”)
- 不依赖
EXCEPTION捕获——它不会抛异常,只是没影响行
关键细节和易踩的坑
乐观锁不是加个 version 字段就完事,这几个点漏掉一个就白搭:
-
version字段必须是NUMBER类型,别用VARCHAR2或浮点——隐式转换或精度误差会让WHERE version = ?失效 - 初始
version必须为 0 或 1,且所有INSERT都要显式赋值,避免 NULL 参与等值判断(NULL = anything永远为 FALSE) - 如果业务要求“扣减失败时自动重试”,得在应用层控制次数和间隔;存储过程内不建议死循环重试——可能卡住会话、耗尽连接
- 别在
WHERE里混用函数,比如AND TO_CHAR(updated_at, 'YYYYMMDD') = '20261026',索引失效,还可能误判
什么时候该放弃乐观锁
当你的场景出现以下任意一种,乐观锁就不是最优解:
- 扣减前必须读取多个字段做复杂校验(比如要同时检查用户等级、历史订单数、风控状态),这些条件无法简洁表达在
WHERE中 - 失败率长期高于 15%——说明冲突太频繁,重试成本已超过加锁开销,该切回
SELECT FOR UPDATE NOWAIT并配好超时处理 - 需要强顺序保障(比如“谁先提交谁成功”,而非“谁先查到旧值谁赢”),这时乐观锁的“最后写入获胜”模型不适用
真正难的不是写对那条 UPDATE,而是想清楚:你到底在保护什么——是数据不超卖?还是业务状态不越权?前者乐观锁够用,后者往往得结合应用层状态机或分布式协调服务。


















