ORA-00060死锁后不可直接用@Retryable重试,因其默认在已回滚事务中执行;必须用TransactionTemplate每次开启新事务,并配合幂等、退避、次数限制(2~3次)及应用层锁。

ORA-00060死锁后不能直接用@Retryable重试
Spring的@Retryable默认在同一个事务上下文中反复执行方法体,而Oracle触发ORA-00060后,victim会话的整个事务已被强制回滚——此时TransactionStatus.isCompleted() == true,但你的Java线程仍在运行。若不检查就重试,Transaction is not active异常必然抛出;更危险的是,若方法里混有非事务性操作(如发MQ、改本地缓存),可能造成状态错乱或重复提交。
必须手动控制事务生命周期 + 显式重试
核心原则:每次重试都得是全新事务,且重试前必须确认连接可用、事务未处于completed状态。推荐用TransactionTemplate替代@Transactional包裹的@Retryable:
-
TransactionTemplate的execute()每次调用都会开启新事务,天然隔离重试上下文 - 重试逻辑写在
catch块里,而不是依赖AOP代理自动触发 - 在重试前加判断:
!txTemplate.getTransactionManager().getTransaction(null).getStatus().isCompleted()(注意:需捕获NoTransactionException) - 避免在
execute()内部再调用另一个@Transactional方法,否则嵌套事务会破坏原子性
@Retryable + @Recover只适用于非事务性远程调用
如果你的场景是「调用外部HTTP服务失败后重试」,@Retryable完全适用;但一旦涉及Oracle DML+事务,它就不再安全。常见误用:
- 在
@Retryable方法上同时加@Transactional——重试时事务已失效,框架可能静默忽略或抛出IllegalTransactionStateException - 把
@Recover当成兜底补偿逻辑——它只是返回一个降级值,无法回滚数据库、清理MQ或修复中间状态 - 没配置
traversingCauses(),导致底层抛出的SQLException被包装成DataAccessException后匹配不到,重试根本不会触发
死锁重试必须配合幂等与退避策略
Oracle死锁不是网络抖动,而是并发资源争抢,盲目重试只会加剧冲突。实际落地要叠加两层控制:
- 退避必须指数增长:
@Backoff(delay = 100, multiplier = 2.0),避免多个线程在毫秒级窗口内再次撞上同一行锁 - 业务层必须保证DML幂等:比如用
INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或MERGE INTO(Oracle),而不是先SELECT再INSERT - 对高冲突字段(如账户余额、库存)加应用层分布式锁(如Redis锁),把数据库锁竞争转移到更轻量的协调层
最易被忽略的一点:重试次数上限建议设为2~3次。超过这个数,大概率说明设计存在根本性问题——比如缺少索引导致锁升级、事务粒度太大、或未按主键顺序访问多行数据。


















