当前位置: 代码网 > it编程>编程语言>Java > SpringBoot循环依赖启动卡死是什么原因?@Async注解的致命影响

SpringBoot循环依赖启动卡死是什么原因?@Async注解的致命影响

2026年09月24日 Java 我要评论
凌晨三点,我被报警短信炸醒——线上服务大面积超时。紧急回滚后,发现是刚上线的“性能优化”代码捅了篓子:一个看似无害的@async注解,引发了spring

凌晨三点,我被报警短信炸醒——线上服务大面积超时。紧急回滚后,发现是刚上线的“性能优化”代码捅了篓子:

一个看似无害的@async注解,引发了springboot容器启动时的循环依赖死锁。

今天我们就来解剖这个经典问题:

为什么springboot的循环依赖有时能启动成功,有时直接卡死?为什么加了@async后会突然暴雷?

1. 事故现场:当“性能优化”变成“性能灾难”

我们的订单系统在促销时面临高并发压力,于是我给orderservice加了个异步处理逻辑:

@service
public class orderservice {
    @autowired
    private paymentservice paymentservice;  // 关键点:交叉依赖
    @async  // 新增的“优化点”
    public void createorder(orderdto dto) {
        // 业务逻辑
    }
}
@service
public class paymentservice {
    @autowired
    private orderservice orderservice;  // 经典的循环依赖
}

上线后,本地测试完美运行。但到了生产环境,服务直接启动超时。为什么?

  • 现象:启动时卡在org.springframework.beans.factory.beancurrentlyincreationexception
  • 诡异点:同样的循环依赖结构,不加@async时能启动成功(spring默认支持单例的setter循环依赖)

2. 根因解剖:spring的三级缓存与代理机制

spring处理循环依赖的核心是三级缓存,但遇到aop代理时,规则会发生变化:

普通bean的循环依赖流程(能成功):

  • 1) orderservice实例化(无属性)→ 放入三级缓存(objectfactory
  • 2) 填充paymentservice属性 → 触发paymentservice实例化
  • 3) paymentservice填充orderservice属性时,从三级缓存拿到半成品orderservice
  • 4) 双方完成初始化

加了@async后的死亡流程

  • @async会通过asyncannotationbeanpostprocessor创建代理对象
  • 代理对象必须在bean初始化完成后才能生成(否则无法确定切面逻辑)
  • 结果:paymentservice在注入orderservice时,spring发现它还在创建中,且无法提前暴露代理对象→死锁
// spring源码中的关键判断(abstractautowirecapablebeanfactory)
if (earlysingletonexposure) {
    // 普通bean能走到这里,提前暴露引用
    addsingletonfactory(beanname, () -> getearlybeanreference(beanname, mbd, bean));
} else {
    // 代理bean可能卡在这里,无法提前暴露
}

3. 破局方案:打破循环依赖的四种姿势

方案1:最暴力——用@lazy延迟加载

@service
public class paymentservice {
    @lazy  // 加上这个注解
    @autowired
    private orderservice orderservice;
}
  • 原理:注入代理对象,首次调用时才触发真实加载。代价:可能把启动时的问题推迟到运行时。

方案2:最优雅——重构代码结构

提取公共逻辑到第三个服务:

@service
public class orderservice {
    @autowired
    private orderpaymentfacade facade;  // 解耦关键
}
@service
public class paymentservice {
    @autowired
    private orderpaymentfacade facade;
}
@service
public class orderpaymentfacade {
    // 放原本需要交叉调用的逻辑
}

方案3:最危险——强行设置proxymode

@async(proxytargetclass = true)  // 尝试强制使用cglib代理
public void createorder() { ... }
  • 慎用:可能引发其他代理冲突(比如与@transactional共用时)。

方案4:最隐蔽——调整bean加载顺序

application.properties中:

spring.main.allow-circular-references=true  # springboot 2.6+专属
  • 警告:这只是让spring不抛异常,本质问题仍在。

4. 性能对比:循环依赖的隐藏代价

我们用jmeter测试同一接口:

方案qps平均耗时启动时间
无循环依赖125038ms8s
普通循环依赖121041ms9s
@async循环依赖崩溃-超时
@lazy方案118043ms11s
  • 结论:即使能启动成功,循环依赖仍会导致约3%~5%的性能损耗。

5. 避坑清单:循环依赖的七个致命陷阱

  • @async/@transactional等aop注解:最容易触发代理导致的启动失败
  • 构造器注入:spring官方明确不支持构造器循环依赖
  • 多线程环境下:即使启动成功,延迟加载可能导致npe
  • @postconstruct方法:在循环依赖中可能被跳过执行
  • 单元测试:@mockbean会破坏spring上下文缓存,暴露隐藏问题
  • springboot版本差异:2.6前后对循环依赖的默认处理策略不同
  • 混合代理模式:cglib和jdk动态代理混用时可能雪上加霜

写完这篇时,我又想起那个凌晨的绝望。

循环依赖就像房间里的隐形大象——平时看不见,一旦它动起来,整个地板都在震颤。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com