SELECT …… FOR UPDATE NOWAIT 不能防止死锁,仅能快速暴露锁冲突并立即报错(如ORA-00054),避免线程挂起;真正防死锁需统一加锁顺序、缩小锁范围及应用层指数退避重试。
直接结论:用 select ... for update nowait 能避免线程挂起等待锁,但不能防止死锁本身;它只是把“等死锁”变成“立刻报错”,从而让应用层有机会重试或降级,属于防御性手段,不是根治方案。
为什么 SELECT FOR UPDATE NOWAIT 不等于防死锁
死锁是多个事务循环等待对方持有的锁(比如 T1 持有 A 行锁、等 B 行锁,T2 持有 B 行锁、等 A 行锁),Oracle 自动检测并抛 ORA-00060。而 NOWAIT 的作用仅限于:当本事务执行 SELECT ... FOR UPDATE NOWAIT 时,若目标行已被其他事务加了排他锁,不排队、不挂起,而是立刻抛 ORA-00054: resource busy and acquire with NOWAIT specified。
它没改变事务加锁顺序,也没干预锁的持有逻辑——所以如果两个事务仍以不同顺序访问同一组行,依然可能在后续的 UPDATE 阶段触发死锁。
-
NOWAIT只作用于当前这条SELECT FOR UPDATE语句,不影响后续 DML - 它不释放已有锁,也不跳过已锁行(那是
SKIP LOCKED干的事) - Java 中捕获
SQLException且getSQLState().equals("61000")或检查getMessage()含ORA-00054才算正确识别
Java 中怎么写才真正起效
关键不是加不加 NOWAIT,而是加在哪儿、配合什么逻辑。单纯加了但没重试或没统一加锁顺序,等于白搭。
- 必须在事务开启后、实际修改前尽早执行
SELECT ... FOR UPDATE NOWAIT,否则可能在UPDATE时才撞上锁,那时已晚 - 不要只对单行查锁,比如转账场景中,应一次性锁定所有涉及账户:
SELECT * FROM account WHERE holder IN (?, ?) FOR UPDATE NOWAIT - Java 里需显式捕获异常并控制重试次数,例如用
@Retryable(Spring Retry)或手动 while 循环 +Thread.sleep,避免瞬间重试压垮 DB - 连接池要配置合理超时(如 HikariCP 的
connection-timeout),防止连接卡在NOWAIT失败后长期闲置
NOWAIT 和 SKIP LOCKED 到底该选谁
适用场景完全不同,混用反而危险。
立即学习“Java免费学习笔记(深入)”;
- 用
NOWAIT:你希望“强一致性”,某条数据此刻不可用就立刻拒绝,比如库存扣减不允许跳过热门商品(秒杀中不许绕开已锁 SKU) - 用
SKIP LOCKED:你接受“最终一致性”,只要能处理任意可用行就行,比如任务队列分发,跳过别人正在处理的 job 行,拿下一个空闲的 - 二者不能共存:
SELECT ... FOR UPDATE NOWAIT SKIP LOCKED是非法语法(Oracle 报ORA-02014) - MySQL 8.0+ 支持两者共存,但 Oracle 不行——这点容易踩坑,尤其团队有跨数据库经验时
真正防死锁,还得靠设计层面
NOWAIT 是兜底手段,根子上的问题得从 Java 层解决。
- 所有涉及多行更新的操作,必须约定全局加锁顺序,比如按主键升序排列后再锁:
SELECT ... WHERE id IN (101, 99) ORDER BY id FOR UPDATE NOWAIT→ 实际锁顺序变成 99、101,避免颠倒 - 避免在事务中调用外部服务(HTTP / RPC),否则锁持有时间不可控,极易引发长事务和锁竞争
- 慎用
SELECT FOR UPDATE在高并发下做“预占位”,它本质是悲观锁,吞吐量天然受限;高频场景优先考虑应用层分布式锁 + 短事务 + 版本号校验 - 监控必须跟上:定期查
v$locked_object和v$session,发现LOCKED_MODE = 3(X 锁)且STATUS = 'INACTIVE'的长时会话,往往是隐患
最常被忽略的一点:NOWAIT 报错后,很多人直接返回失败,但没清理本地缓存或下游状态,导致业务语义错乱。错误处理必须和业务补偿对齐,而不是只盯着 SQL 是否成功。


















