必须用显式异常捕获+SAVEPOINT+随机延迟+指数退避,否则重试大概率再次死锁;因固定或立即重试无法错开竞争窗口,导致两个会话在毫秒级内重复相同资源争抢顺序,而Oracle死锁检测异步触发,未扰动的重试使冲突原样复现。

必须用显式异常捕获 + SAVEPOINT + 随机延迟 + 指数退避,否则重试大概率再次死锁。
为什么不能只用固定延迟或立即重试
两个会话同时遇到 ORA-00060 后若都执行 DBMS_LOCK.SLEEP(0.01),几乎必然在毫秒级窗口内再次申请相同资源顺序,复现死锁。Oracle 的死锁检测是异步触发的,重试时机没错开,竞争关系原封不动。
- 固定延迟(如统一休眠 100ms)会导致“重试共振”,尤其在高并发批量任务中
- 不加延迟的立即重试等同于把失败逻辑再跑一遍,事务状态已破坏,
ROLLBACK TO SAVEPOINT也救不回来 - 指数退避的核心是让重试时间分布更松散:第 1 次等约 50ms,第 2 次约 150ms,第 3 次约 450ms —— 这个范围需叠加随机扰动
PL/SQL 中实现指数退避重试的正确结构
关键不是循环本身,而是每次迭代前重置事务边界、控制延迟、限制次数。以下结构不可省略任何一环:
- 外层用
LOOP包裹整个业务逻辑,起始设SAVEPOINT sp_start - 每次进入循环前调用
DBMS_RANDOM.VALUE(0, 0.1)生成随机偏移,再叠加指数基值(如POWER(1.5, retry_count) * 0.05) - 捕获必须显式写成
WHEN OTHERS THEN IF SQLCODE = -60 THEN ...,不能塞进WHEN OTHERS后统一判断,否则堆栈丢失、调试无从下手 - 重试计数器
retry_count必须在循环外声明并初始化为 0,每次失败后retry_count := retry_count + 1,超过 3 就RAISE
示例片段(非完整过程,仅核心逻辑):
DECLARE
retry_count PLS_INTEGER := 0;
max_retries CONSTANT PLS_INTEGER := 3;
base_delay NUMBER := 0.05;
sleep_time NUMBER;
BEGIN
LOOP
SAVEPOINT sp_start;
BEGIN
-- 你的 DML 操作,如 UPDATE / INSERT
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
EXIT; -- 成功则跳出循环
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE = -60 THEN
retry_count := retry_count + 1;
IF retry_count > max_retries THEN
RAISE_APPLICATION_ERROR(-20999, 'Deadlock retry exhausted');
END IF;
-- 计算指数退避 + 随机扰动
sleep_time := POWER(1.5, retry_count) * base_delay + DBMS_RANDOM.VALUE(0, 0.05);
DBMS_LOCK.SLEEP(sleep_time);
ROLLBACK TO sp_start;
ELSE
RAISE; -- 非死锁异常不重试
END IF;
END;
END LOOP;
COMMIT;
END;
容易被忽略的三个硬性约束
这些不是建议,是 Oracle 事务模型决定的强制规则,违反即导致数据不一致或过程崩溃:
-
COMMIT只能出现在循环之外、最终成功路径上;循环内任何COMMIT会让后续ROLLBACK TO SAVEPOINT失效 - 每次重试前必须
ROLLBACK TO sp_start,不能依赖隐式回滚 —— 因为部分语句可能已成功执行(如第一个UPDATE),残留状态会污染下一轮 -
DBMS_LOCK.SLEEP是唯一安全的休眠方式;用DBMS_SESSION.SLEEP在某些 Oracle 版本中会抛ORA-01031权限错误,且它不释放 CPU 资源
真正难的不是写对语法,而是理解:重试不是“再试一次”,而是“放弃当前事务上下文,用新时间点、新资源顺序、新竞争概率重新发起”。指数退避和随机化,本质是在人为制造时间差与行为差,让系统有机会自然解耦。











