开发故事:一个 @Async 如何搞瘫整个微服务系统

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 大家好,我是小米,一个热爱分享技术的29岁开发者。本文讲述了一个困扰我们团队的开发环境问题,最终发现罪魁祸首竟是 `@Async` 注解。我们通过详细分析错误日志和 Spring 的 Bean 代理机制,逐步排查并解决了这一难题。文章介绍了三种解决方案:调整依赖结构、使用 `@Lazy` 延迟加载以及禁用 `@Async` 的代理功能。希望对你有所帮助!欢迎关注我的微信公众号“软件求生”,获取更多技术干货!



嘿,大家好!我是小米,一个充满活力、喜欢分享技术的29岁开发者。今天的文章,我们要来聊一聊一个发生在我们开发环境的惊险故事。这个问题折腾了我们整个团队好一阵子,最终我们发现元凶竟然是一个看似无害的 @Async 注解。废话不多说,直接开讲!

故事的开始:微服务无法启动

就在昨天,我们的开发环境微服务无法启动,连续尝试了好几次,每次启动的时候日志都报错:

读完这个错误日志,我们开发团队的小伙伴们都挠了挠头,心想这到底是个啥?特别是我们刚刚合并的代码中也没有涉及到什么大改动,为什么会突然冒出这么个问题?

错误日志解析

首先,让我们来分析一下这个错误日志究竟在说什么。

  • Bean 被注入到其他 Beans 中:错误提示说,tradeService 这个 Bean 被注入到了其他两个 Bean(returnOrderServicerefundOrderService)中,这本身其实是没什么问题的。在 Spring 的依赖注入机制下,Bean 被注入到其他 Bean 中是一件再普通不过的事。
  • Raw version 被注入:问题的关键是这里提到的 “raw version”(原始版本)。在 Spring 中,Bean 经过创建和初始化后,可能还会被代理包装。比如,AOP 或者 @Async 会通过动态代理对 Bean 进行增强。那么这里的意思是,tradeService 被注入时,其他服务接收到的不是它的最终版本,而是未经过增强的原始版本。
  • Circular reference(循环引用)问题:这个提示还提到了可能存在的循环引用问题。Spring 为了避免 Bean 的循环依赖,采用了三级缓存机制。在 Bean 创建过程中,如果有 Bean 依赖另一个还未完全初始化完成的 Bean,Spring 会暂时将原始 Bean 注入,这样可以打破循环引用的死锁。
  • “over-eager type matching”:这个错误的最后部分提到了可能的原因:过度积极的类型匹配。在某些情况下,Spring 容器会过早地初始化某些 Bean,这可能会导致一些尚未完全准备好的 Bean 被注入。

至此,我们已经大致明白问题的轮廓:tradeService 这个 Bean 因为某种原因,被注入了它的原始版本,而不是最终被代理增强后的版本。

团队排查:@Async 的可疑之处

我们开始逐步排查代码变更,最后注意到一个开发同事在 tradeService 的某个方法上添加了 @Async 注解。大家知道,@Async 是 Spring 提供的一个非常方便的异步执行注解。通过在方法上加上这个注解,Spring 会把这个方法的执行交给一个线程池,避免占用主线程资源。

虽然 @Async 本身非常好用,但它的实现依赖于 AOP 动态代理机制。当 Spring 看到一个方法被 @Async 注解修饰时,它会创建一个代理对象,代理对象接管方法的调用逻辑。在这种情况下,如果你直接在其他地方引用了这个 Bean 的原始版本,而不是代理后的版本,就会导致预期外的行为——比如像我们遇到的错误。

正是这个 @Async 导致了 Bean 没有被完整初始化,进而引发了循环依赖和代理注入的问题。

深入解析:Spring 的代理机制与循环依赖

为了更好地理解这个问题,我们需要进一步探讨 Spring 是如何处理 Bean 的依赖注入和代理机制。

1. Bean 的创建过程

在 Spring 中,Bean 的创建分为以下几个步骤:

  1. 实例化:首先,Spring 会通过构造函数或者工厂方法实例化 Bean。
  2. 依赖注入:接下来,Spring 会为这个 Bean 注入所需的依赖,这时候如果某个 Bean 依赖还没有准备好,Spring 就会遇到循环依赖问题。
  3. 初始化:在依赖注入完成后,Spring 会执行 Bean 的初始化方法,比如 @PostConstruct 标注的方法,或者执行一些自定义的初始化逻辑。
  4. 代理包装:在初始化完成后,如果这个 Bean 需要增强(如 AOP 或者 @Async),Spring 会为这个 Bean 创建一个代理对象。

2. 循环依赖

如果两个 Bean 相互依赖,比如 A 依赖 B,而 B 又依赖 A,那么 Spring 就会遇到循环依赖问题。为了打破这种循环,Spring 会在依赖注入时注入原始版本的 Bean,而不是最终被代理包装后的版本。

在我们的案例中,tradeServicereturnOrderServicerefundOrderService 引用,而它自身因为使用了 @Async 注解,导致它需要被代理。如果 Spring 在创建代理之前就将它注入到了其他服务中,就会引发前面提到的那个错误。

找到了问题的根源,我们就可以着手解决它了。

方法一:调整依赖结构

最直接的解决方案就是重新审视我们的依赖结构。我们可以考虑是否能打破循环引用,避免 tradeService 被过早地注入到其他 Bean 中。

在我们的案例中,我们尝试通过调整依赖关系,将 tradeService 的一些依赖注入拆分到其他组件中,避免直接注入原始 Bean。

方法二:使用 @Lazy 延迟加载

如果调整依赖结构有些困难,另一种方法是使用 @Lazy 注解。通过将 Bean 设置为懒加载,Spring 会推迟它的实例化,直到它真正被使用时才会创建。这可以有效避免 Bean 在初始化过程中被过早地注入。

方法三:禁用 @Async 的代理

在某些场景下,如果 @Async 只是在某些方法上使用,而不是全局依赖,我们可以考虑不使用代理。在 Spring 中,可以通过配置文件或者编程的方式禁用某些 Bean 的代理行为。

但这种方式比较少见,通常我们会选择保留 @Async 代理,毕竟它为我们提供了异步执行的便利。

END

这次开发环境中的微服务启动问题,最终是由 @Async 引起的代理问题导致的。通过深入理解 Spring 的 Bean 代理机制和循环依赖的处理方法,我们成功找到了问题的根源,并采取了有效的解决方案。这个案例告诉我们,在使用诸如 @Async 这样的注解时,一定要小心其背后的代理机制,尤其是在存在复杂依赖关系的场景下。

如果你也遇到类似的问题,记得从错误日志中寻找线索,逐步排查问题的根源。希望这篇文章对大家有所帮助!

我是小米,一个喜欢分享技术的29岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号软件求生,获取更多技术干货!

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
19天前
|
Kubernetes 负载均衡 微服务
Kubernetes 生态系统中的微服务治理
【8月更文第29天】随着微服务架构的普及,管理分布式系统的复杂性也随之增加。Kubernetes 作为容器编排的事实标准,为微服务架构提供了强大的支持。结合像 Istio 这样的服务网格工具,Kubernetes 能够有效地解决微服务治理中的诸多挑战,如服务发现、负载均衡、流量管理和安全策略等。
27 1
|
19天前
|
Java UED Sentinel
微服务守护神:Spring Cloud Sentinel,让你的系统在流量洪峰中稳如磐石!
【8月更文挑战第29天】Spring Cloud Sentinel结合了阿里巴巴Sentinel的流控、降级、熔断和热点规则等特性,为微服务架构下的应用提供了一套完整的流量控制解决方案。它能够有效应对突发流量,保护服务稳定性,避免雪崩效应,确保系统在高并发下健康运行。通过简单的配置和注解即可实现高效流量控制,适用于高并发场景、依赖服务不稳定及资源保护等多种情况,显著提升系统健壮性和用户体验。
46 1
|
25天前
|
设计模式 数据管理 测试技术
后端开发中的微服务架构设计哲学
【8月更文挑战第23天】在软件开发的海洋中,微服务架构如同一艘精心设计的船只,它以独特的设计理念和航行技巧,引领着后端开发的未来。本文将探讨微服务的核心概念、设计原则以及如何将这些理念融入到日常的开发实践中,旨在为读者提供一套清晰的微服务设计指南。
|
28天前
|
监控 负载均衡 数据管理
后端开发中的微服务架构实践与挑战
【8月更文挑战第20天】在现代软件工程领域,微服务架构因其灵活性和可扩展性而受到推崇。本文将深入探讨微服务架构的核心概念、实施过程中的关键步骤以及面临的主要挑战,旨在为后端开发人员提供一个全面的视角,帮助他们理解和应对微服务架构带来的变革。
|
28天前
|
运维 监控 持续交付
后端开发中的微服务架构:优势与挑战
【8月更文挑战第20天】随着云计算和容器化技术的发展,微服务架构已经成为现代软件开发中的一种流行趋势。本文将探讨微服务架构的核心优势以及在实施过程中可能遇到的技术和组织挑战。我们将从微服务的定义入手,进而深入分析其设计哲学、技术特性以及在实际应用中的效益与问题。
156 56
|
4天前
|
运维 Cloud Native Devops
云原生架构的崛起与实践云原生架构是一种通过容器化、微服务和DevOps等技术手段,帮助应用系统实现敏捷部署、弹性扩展和高效运维的技术理念。本文将探讨云原生的概念、核心技术以及其在企业中的应用实践,揭示云原生如何成为现代软件开发和运营的主流方式。##
云原生架构是现代IT领域的一场革命,它依托于容器化、微服务和DevOps等核心技术,旨在解决传统架构在应对复杂业务需求时的不足。通过采用云原生方法,企业可以实现敏捷部署、弹性扩展和高效运维,从而大幅提升开发效率和系统可靠性。本文详细阐述了云原生的核心概念、主要技术和实际应用案例,并探讨了企业在实施云原生过程中的挑战与解决方案。无论是正在转型的传统企业,还是寻求创新的互联网企业,云原生都提供了一条实现高效能、高灵活性和高可靠性的技术路径。 ##
12 3
|
6天前
|
消息中间件 存储 缓存
后端开发之深入浅出微服务架构
在数字化时代的浪潮中,后端开发如同一座桥梁,连接着用户与数据的世界。本文将带你探索微服务架构的奥秘,从基础概念到实战应用,一步步揭开它神秘的面纱。我们将一起思考,如何在这个快速变化的时代,找到属于自己的节奏和方向。
18 2
|
11天前
|
缓存 Java 应用服务中间件
随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架
【9月更文挑战第6天】随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架。Nginx作为高性能的HTTP反向代理服务器,常用于前端负载均衡,提升应用的可用性和响应速度。本文详细介绍如何通过合理配置实现Spring Boot与Nginx的高效协同工作,包括负载均衡策略、静态资源缓存、数据压缩传输及Spring Boot内部优化(如线程池配置、缓存策略等)。通过这些方法,开发者可以显著提升系统的整体性能,打造高性能、高可用的Web应用。
37 2
|
23天前
|
存储 API 持续交付
探索微服务架构:构建灵活、可扩展的后端系统
【8月更文挑战第25天】 本文将引导您理解微服务架构的核心概念,探讨其对现代后端系统设计的影响。我们将从基础讲起,逐步深入到微服务的高级应用,旨在启发读者思考如何利用微服务原则优化后端开发实践。
38 4
|
24天前
|
负载均衡 Cloud Native 中间件
核心系统转型问题之微服务架构并存的问题如何解决
核心系统转型问题之微服务架构并存的问题如何解决