同事说Spring循环依赖很简单,直到我们线上炸了...

场景还原

周一早上,刚到工位,运维群里炸了:

erlang 复制代码
【告警】订单服务大量超时
【告警】用户服务CPU 100%
【告警】订单服务OOM

我心想:完蛋,又出事了。

问题现场

运维查了半天,说:"好像是Spring启动的时候就有问题了,一直没暴露出来。"

同事小王说:"肯定是循环依赖!我之前就说过,Spring循环依赖很简单,三级缓存了解一下就行了。"

我问他:"那你倒是解决一下啊。"

他沉默了。

什么是循环依赖?

less 复制代码
@Service
public class AService {
    @Autowired
    private BService bService;  // A依赖B
}

@Service
public class BService {
    @Autowired
    private AService aService;  // B依赖A
}

A等B,B等A,死循环

就像:

  • 你在等女朋友出门
  • 女朋友在等你开车
  • 你俩在楼下等了对方一晚上

Spring是怎么解决的?

Spring用三级缓存

级别 缓存 作用
一级 singletonObjects 成品Bean,直接用
二级 earlySingletonObjects 半成品Bean,提前暴露
三级 singletonFactories Bean工厂,用于创建代理

解决流程:

css 复制代码
// 1. 创建A,发现需要B
// 2. 创建B,发现需要A
// 3. 三级缓存里有A的工厂,提前创建一个半成品A
// 4. B创建完成,放到一级缓存
// 5. A拿到B,创建完成

简单说:我先给你一个"欠条",等你准备好了再来找我拿。

但是!

构造器注入的循环依赖解决不了!

kotlin 复制代码
@Service
public class AService {
    public AService(BService b) {  // 构造器注入
        this.bService = b;
    }
}

构造器注入必须在创建对象时就注入,没法提前给欠条

我们的线上问题

less 复制代码
@Service
public class OrderService {
    @Autowired
    private UserService userService;
    
    @Transactional
    public void createOrder() {
        // 创建订单
    }
}

@Service
public class UserService {
    @Autowired
    private OrderService orderService;
    
    @Transactional
    public void createUser() {
        // 创建用户
    }
}

OrderService和UserService互相依赖,而且都用@Transactional。

启动的时候就开始互相等,等着等着,事务嵌套事务,事务嵌套事务...

最后OOM了。

解决方案

方案一:重构代码,消除循环依赖

less 复制代码
// 把公共逻辑抽取出来
@Service
public class CommonService {
    // 公共逻辑放这里
}

@Service
public class OrderService {
    @Autowired
    private CommonService commonService;  // 不再依赖UserService
}

方案二:用@Lazy延迟加载

less 复制代码
@Service
public class OrderService {
    @Autowired
    @Lazy  // 延迟注入,用的时候再加载
    private UserService userService;
}

方案三:用ObjectProvider

java 复制代码
@Service
public class OrderService {
    private final ObjectProvider<UserService> userServiceProvider;
    
    public OrderService(ObjectProvider<UserService> userServiceProvider) {
        this.userServiceProvider = userServiceProvider;
    }
    
    public void doSomething() {
        UserService userService = userServiceProvider.getObject();  // 用的时候再拿
        userService.doSomething();
    }
}

血泪教训

  1. 循环依赖不是小事:特别是有@Transactional的时候
  2. 三级缓存不是万能的:构造器注入就解决不了
  3. 代码设计很重要:尽量避免循环依赖,从源头杜绝
  4. 上线前做启动测试:别等线上炸了才发现
相关推荐
To_OC4 小时前
手写 AI 编程 Agent 的命令执行工具:我被 child_process 坑出来的实战经验
后端·node.js·agent
逝水无殇6 小时前
C# 异常处理详解
开发语言·后端·c#
考虑考虑9 小时前
Sentinel安装
java·后端·微服务
IT_陈寒9 小时前
SpringBoot自动配置失灵?你可能忘了这个关键注解
前端·人工智能·后端
碎碎念_4929 小时前
SpringBoot + Vue 前后端分离从 0 到 1 完整环境配置流程
vue.js·spring boot·后端
逝水无殇9 小时前
C# 文件的输入与输出详解
开发语言·数据库·后端·c#
YIAN10 小时前
从手写 Agent 工具到 MCP 协议:一文搞懂大模型跨进程跨语言工具调用
后端·agent·mcp
小兔崽子去哪了10 小时前
Docker 删除镜像后磁盘空间没有释放?
后端·docker·容器
高明珠11 小时前
麒麟 V10 SP1 排查实录:ttyS5 每 10 秒莫名收到一批报文,元凶是 eGTouchD
后端
卷无止境11 小时前
Python FFI 技术深度解析:ctypes、cffi 与 pybind11 的性能差异与实践挑战
后端·python