同事说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. 上线前做启动测试:别等线上炸了才发现
相关推荐
小兔崽子去哪了10 分钟前
Vue3 + Pinia 集成 IGV.js 实现 BAM 文件在线浏览
javascript·vue.js·后端
孟陬17 分钟前
Claude Code 巧思 `Ctrl+S` 暂存键
前端·后端
雪隐28 分钟前
个人电脑玩AI-06让5060 Ti给你打工——不光能画画,Qwen3-TTS还能学人说话,连我老板都信了!
人工智能·后端·python
Oneslide1 小时前
openEuler 17.1GB Everything ISO 离线本地 DNF 源搭建教程
后端
蝎子莱莱爱打怪1 小时前
那不是我的黑历史,那是我的来时路啊!😭😭
后端·程序员
用户298698530141 小时前
Java 实现 Word 文档文本与图片提取的方法
java·后端
蝎子莱莱爱打怪1 小时前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
Csvn1 小时前
Rsync 文件同步与增量备份 — 运维的数据守门员
后端
苏三说技术1 小时前
推荐一个牛逼的智能代码审查系统
后端
倾颜1 小时前
从 GitHub Actions 到本地兜底发布:AI Mind 容器化上线的一次真实收口
后端