同事说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. 上线前做启动测试:别等线上炸了才发现
相关推荐
XovH41 分钟前
环境搭建与第一个“Hello, World”:Django 项目结构与 MTV 模式详解
后端
阿丰资源1 小时前
基于SpringBoot的电影评论网站(含源码)
java·spring boot·后端
小码哥0681 小时前
2026版基于springboot的家政服务预约系统
java·spring boot·后端
浪荡Ddddd1 小时前
初识SpringAI:chat篇
后端·程序员
小谢小哥1 小时前
59-消息推送系统详解
java·后端·架构
杨运交1 小时前
[016][web模块]基于 MDC 的分布式追踪框架设计与实现
spring boot·后端
panshihao1 小时前
SSE 是什么?从原理到实战(Java+Vue+Node全示例)
java·后端·http
贺国亚1 小时前
线程基础与生命周期- 并发编程
java·后端
传说之后1 小时前
Go语言并发安全入门指南
后端