同事说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. 上线前做启动测试:别等线上炸了才发现
相关推荐
IT_陈寒27 分钟前
Vue的响应式更新把我坑惨了,原来问题出在这
前端·人工智能·后端
陌シ未央ゞ1 小时前
基于BM25算法和RRF实现的混合索引(java版)
人工智能·spring boot·后端·算法
第五页的你1 小时前
SpringBoot基础设施配置(Redis序列化,JJWT新版)
后端
吃饱了得干活1 小时前
RabbitMQ 原理解析(下):存储、集群与可靠投递
后端·rabbitmq
SelectDB2 小时前
某头部保险公司基于 SelectDB 的统一 OLAP 架构实践
后端
ServBay2 小时前
ServBay 1.33.0 来了:AI Gateway 一键接管主流 AI CLI 与多模型
后端·aigc·ai编程
行者全栈架构师2 小时前
Spring Boot 接入 MaxKey 单点登录:6 个内部系统,一次登录全通行
java·vue.js·后端
JuiceFS2 小时前
卓驭:百 PB 级智驾数据存储架构演进
后端·自动驾驶
鱼弦2 小时前
跨模态迁移的极限:语言模型的逻辑能力能否完全迁移到视觉?
后端
ModStart2 小时前
写歌、翻唱、可编辑乐谱,YuE2-3B 在 AIGCPanel 一键跑通
后端