select for update必须在显式事务中才有效,否则锁在语句结束即释放;where条件未走索引会导致全表扫描并锁整表;wait n比nowait更适合防重复处理;skip locked是多消费者并发取任务的唯一解。

SELECT FOR UPDATE必须在显式事务里才有效
裸执行SELECT ... FOR UPDATE,锁在语句结束时就释放了——哪怕你紧接着写UPDATE,中间也存在竞态窗口。Oracle 默认开启自动提交(AUTOCOMMIT),每条语句独立成事务,锁毫无意义。
必须先关自动提交,再包事务:
- SQL*Plus/SQLcl 中执行
SET AUTOCOMMIT OFF - JDBC 应用中调用
connection.setAutoCommit(false) - PL/SQL 存储过程中用
BEGIN ... COMMIT; END;显式包裹
锁只在 COMMIT 或 ROLLBACK 后释放,不是语句执行完就松。断开连接未提交?锁会一直挂着,直到超时(默认 60 秒)或 DBA 干预。
WHERE 条件不走索引=锁整张表
这是生产事故最常见原因。Oracle 不按“你写了 WHERE id = 100”就只锁那一行——它看执行计划。如果条件列没索引、用了函数(如 UPPER(name) = 'ALICE')、或发生隐式类型转换(比如传字符串匹配 NUMBER 字段),就会退化为全表扫描,所有被扫描的行都被加锁,等效于表级阻塞。
验证方式固定三步:
- 执行
EXPLAIN PLAN FOR SELECT * FROM t WHERE id = 100 FOR UPDATE; - 查
PLAN_TABLE,确认OPERATION是INDEX RANGE SCAN或类似 - 检查
ACCESS_PREDICATES是否包含你的索引字段,且非空
主键、唯一约束列天然安全;普通字段务必建 B-tree 索引,别信“小表不用索引”这种说法。
WAIT n 比 NOWAIT 更适合防重复处理
FOR UPDATE NOWAIT 一遇到锁就报 ORA-00054,应用层只能立刻失败。这对秒杀类“要么马上抢到,要么放弃”场景合适,但对后台任务队列、订单状态更新这类需要“尽力而为”的逻辑,反而制造雪崩:多个线程同时失败→重试→再次冲突→循环恶化。
FOR UPDATE WAIT 3(单位秒)才是可控选择:
- 最多等 3 秒,超时抛
ORA-30006,可捕获后做指数退避重试(1s → 2s → 4s) - 超过最大重试次数(建议 ≤3),应降级为异步补偿或返回用户提示“处理中,请稍候”
- 别设太长,比如
WAIT 30,可能让线程长时间挂起,拖垮连接池
注意:WAIT 时间受数据库参数 enqueue_resources 影响,高并发下该值不足会提前报错,需协同调优。
SKIP LOCKED 是多消费者并发取任务的唯一解
当多个工作进程(worker)从同一张任务表(如 job_queue)里争抢待处理记录时,用 FOR UPDATE NOWAIT 或 WAIT 都会互相卡住——谁都拿不到,或轮流失败。
FOR UPDATE SKIP LOCKED 直接跳过已被其他事务锁定的行,只返回当前可用的记录集:
- 每个 worker 执行一次,拿到不同行,互不阻塞
- 必须配合
ROWNUM 或 <code>FETCH FIRST 1 ROW ONLY,否则可能一次锁太多行 - Oracle 11gR2+ 支持,低版本别用;不能和
NOWAIT同时出现,语法冲突
它不适合要求“严格顺序处理”的场景——跳过的行下次可能被别的 worker 拿走,业务上得接受这个不确定性。
真正难的不是写出SELECT ... FOR UPDATE,而是判断哪一行该锁、锁多久、锁不住时业务怎么兜底。索引失效、自动提交陷阱、锁等待策略选错,任何一个点没踩准,都会让悲观锁变成性能黑洞。











