同事说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. 上线前做启动测试:别等线上炸了才发现
相关推荐
JustHappy1 天前
古法编程秘籍(二):什么是代码模块化?别背概念,把房间收拾明白就够了
前端·后端
小江的记录本1 天前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
IT_陈寒1 天前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端
IT_陈寒1 天前
Java注解空指针?这个坑我踩得莫名其妙
前端·人工智能·后端
土狗TuGou1 天前
SQL内功笔记 · 第8篇:事务的四大特性与隔离级别
数据库·笔记·后端·sql·mysql·oracle
ZengLiangYi1 天前
React Query + REST API 最佳实践
javascript·后端·react.js
星浩AI1 天前
项目实战:合同智能审批 · LangGraph + HITL 人机协同方案 [有源码]
后端·langchain·agent
JavaGuide1 天前
Codex 接入第三方模型 DeepSeek、GLM、Kimi 教程:CC-Switch 和 Codex++ 两种方案对比
后端·ai编程
ZengLiangYi1 天前
Fastify 加 Electron:把 Web 服务嵌进桌面应用
前端·javascript·后端
李白你好1 天前
页面资产梳理 · 技术指纹识别 · Spring 端点探测
java·后端·spring