必须用 ex.geterrorcode() == 60 && "61000".equals(ex.getsqlstate()) 判断 ora-00060,因 getmessage() 易受截断、翻译影响;@retryable 与 @transactional 组合无效,因死锁异常发生在提交阶段,需改用 transactiontemplate 或手动事务控制重试。

ORA-00060 在 Spring Boot 里不能靠 getMessage() 字符串匹配来识别,必须用 ex.getErrorCode() == 60 && "61000".equals(ex.getSQLState()) ——这是唯一跨驱动版本、不受日志截断或国际化影响的判断方式。
怎么准确捕获 ORA-00060 异常
Spring 的 @Transactional 方法抛出死锁异常时,实际发生在事务提交阶段(TransactionSynchronization.afterCompletion),此时业务方法早已返回,AOP 代理无法拦截。直接在方法上加 @Retryable 是无效的。
-
ex.getErrorCode()恒为60,ex.getSQLState()恒为"61000",二者缺一不可;只判errorCode虽大概率够用,但极少数 Oracle 驱动变体可能复用错误码,加SQLState校验更稳 - 绝对禁用
ex.getMessage().contains("deadlock")或"ORA-00060":日志中插入调试语句、开启 SQL trace、驱动升级都可能导致字符串不完整或被翻译 - Spring Retry 默认不重试 checked exception,
SQLException是 checked 的,必须显式配置include = { SQLException.class }
为什么 @Transactional + @Retryable 组合会失效
死锁异常不是在业务逻辑执行中抛出的,而是在 Spring 事务管理器调用 connection.commit() 时由 JDBC 驱动触发。此时方法栈已退出,@Retryable 的代理切面早已失去控制权。
- 方案一:把核心 DML 拆到无
@Transactional注解的私有方法中,外层方法手动开启事务 +try-catch+ 重试逻辑 - 方案二:改用
TransactionTemplate显式控制事务,在catch块里调用txTemplate.execute(),确保每次重试都是全新事务上下文 - 严禁在重试块内嵌套
@Transactional方法——这会创建新事务,破坏原事务原子性,还可能让重试逻辑误认为“上一次已成功”
重试前必须确认事务状态和连接可用性
Oracle 触发 ORA-00060 后,victim 会话的整个事务已被强制回滚,但你的 Java 线程还在运行。若不检查就重试,轻则报 Transaction is not active,重则造成重复插入或状态错乱。
- 重试前必须确认事务未完成:
TransactionStatus.isCompleted() == false(Spring)或connection.getAutoCommit() == false && !connection.isClosed() - 每次重试前要重新获取连接,不能复用已标记为
rollbackOnly的旧连接——否则commit()会直接抛异常 - 仅允许重试纯 DML(
INSERT/UPDATE/DELETE),禁止对SELECT FOR UPDATE或含 HTTP 调用、文件操作的逻辑重试 - 建议最多重试 3 次,第二次起加随机休眠:
Thread.sleep(50 + new Random().nextInt(100)),避免重试风暴加剧锁竞争
比重试更重要的:避免死锁本身
重试只是兜底手段。80% 的 ORA-00060 来自多模块更新顺序不一致,比如 A 服务先 UPDATE orders 再 UPDATE order_items,B 服务反过来——这种设计问题不会因重试而消失。
真正该做的,是统一所有服务对关联表的更新顺序,并确保 WHERE 条件字段都有索引(避免全表扫描导致加锁顺序不可控)。ORA-00060 的 trace 文件里 Deadlock graph 才是唯一真相来源,它明确写出谁持有什么锁、请求什么锁。不看这个图就动手改代码,大概率修了表象、漏了根子。











