存储过程本身不带锁,因其仅为PL/SQL代码块,在各自会话事务中运行,不自动锁定数据;若无显式SELECT...FOR UPDATE等锁机制,多会话并发更新同一行将导致覆盖写。

Oracle中多存储过程并发执行时,不能靠存储过程自身“加锁”,必须显式使用 SELECT ... FOR UPDATE 或事务级行锁机制,否则根本不存在真正意义上的悲观锁控制。
为什么存储过程本身不带锁
存储过程只是封装好的 PL/SQL 代码块,执行时默认在各自会话的事务上下文中运行。它不会自动锁定任何数据——哪怕两个过程都更新同一张表的同一行,若没加锁语句,就会发生覆盖写(lost update)。常见错误是以为“调用同一个存储过程”就天然串行化,实际完全不是。
-
CREATE OR REPLACE PROCEDURE定义本身不携带锁语义 - 过程内若只写
UPDATE ... WHERE id = ?,没有前置SELECT ... FOR UPDATE,就是纯乐观更新逻辑 - 多个会话并发执行该过程,可能同时读到旧值、同时计算、同时写入,后提交者覆盖前提交者
正确做法:在过程内嵌入 SELECT ... FOR UPDATE
悲观锁的核心动作是“先查后锁”,必须在修改前锁定目标行,并确保整个操作在同一个事务中完成。典型结构如下:
CREATE OR REPLACE PROCEDURE update_balance(p_acct_id NUMBER, p_amount NUMBER) AS
v_curr_bal NUMBER;
BEGIN
-- 关键:显式加锁,且必须指定 WAIT 或 NOWAIT 行为
SELECT balance INTO v_curr_bal
FROM accounts
WHERE acct_id = p_acct_id
FOR UPDATE WAIT 5; -- 等待最多 5 秒,超时抛 ORA-30006
<p>IF v_curr_bal + p_amount < 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Insufficient balance');
END IF;</p><p>UPDATE accounts SET balance = balance + p_amount
WHERE acct_id = p_acct_id;</p><p>COMMIT; -- 显式提交释放锁;若用自治事务则另当别论
EXCEPTION
WHEN TIMEOUT_ON_RESOURCE THEN
RAISE_APPLICATION_ERROR(-20002, 'Account locked by another session');
END;-
FOR UPDATE WAIT n是生产环境首选,避免无限阻塞 - 不要省略
INTO变量——SELECT ... FOR UPDATE必须有结果集绑定,否则语法报错 - 锁范围由
WHERE条件决定:条件越精确(如主键),锁粒度越小,竞争越低 - 如果过程被频繁调用,
WAIT 0(即NOWAIT)可快速失败,由应用重试或降级处理
容易踩的坑:事务边界与锁生命周期
锁只在事务结束(COMMIT 或 ROLLBACK)时释放。很多开发者忽略这点,导致锁意外长期持有:
- 忘记
COMMIT或ROLLBACK—— 锁持续到会话断开,可能卡住其他业务 - 在包(package)中使用全局变量缓存连接状态,误以为“过程结束=事务结束”,其实事务仍活跃
- 调用链中混用自治事务(
AUTONOMOUS_TRANSACTION):自治事务内的FOR UPDATE不影响外层事务锁,极易造成逻辑错乱 - 异常未捕获导致事务未回滚:
WHEN OTHERS THEN NULL是高危写法,应至少ROLLBACK并重抛
复杂点在于锁等待链和死锁风险
多个存储过程若按不同顺序访问多张表(如过程 A 先锁 orders 再锁 inventory,过程 B 反之),就可能触发 Oracle 自动检测到的死锁(ORA-00060)。这不是代码写错,而是设计缺陷:
- 所有涉及悲观锁的过程,必须严格约定资源访问顺序(如总是先锁主表再锁明细表)
- 避免在循环中逐条
SELECT ... FOR UPDATE,尤其当循环条件不可控时,锁数量和时间不可预估 - 监控锁等待最直接的方式是查
V$SESSION_WAIT和V$LOCK,但线上出问题时往往已晚——关键在设计阶段就规避
真正难的不是写对一行 FOR UPDATE,而是让所有相关过程在并发路径上保持一致的锁策略、顺序和事务粒度。一旦跨过程协作,单点加锁解决不了系统级竞争。


















