Spring Boot自动装配实战:多数据源SDK解决Dubbo性能瓶颈

Spring文章专栏:https://juejin.cn/column/7511884538579877939

明明学了自动装配,却鲜有机会实战?当我面对Dubbo性能瓶颈时,一个自定义Starter的构想让我开启了Spring Boot条件化装配的奇妙之旅。

引言:那些年我们学过的自动装配

记得毕业那会刚开始学习Spring Boot的时候,自动装配机制让我眼前一亮——"约定大于配置"的理念真是太巧妙了!相信很多小伙伴都和我一样,怀着好奇心去研究@EnableAutoConfigurationspring.factories的奥秘,甚至动手尝试编写过自己的Starter。

但说实话,在实际项目开发中,真正需要自己实现自动装配的场景并不多。大多数时候,我们都是在使用Spring Boot官方或者第三方提供的Starter。直到最近,我遇到了一个实实在在的需求,才让我有机会深入实践这个机制。

背景:Dubbo调用成了性能瓶颈

我在公司参与的这个大型项目采用了典型的微服务架构,各个服务之间通过Dubbo进行调用。项目规模较大,因此分成多个开发小组,每个小组负责不同的微服务模块。

随着业务量增长,我们发现了一个棘手的问题:某些高频的数据查询操作通过Dubbo调用时,性能开销变得不可忽视。虽然单次调用的延迟不大,但在高并发场景下,这些开销累积起来就相当可观了。同时提供duboo的服务,因为高频调用已经存在并发瓶颈,频繁告警,如果继续增加调用量随时可能崩溃。(因为数据库规格较高,瓶颈不在于数据库,而只在于dubbo服务提供方,且因为各种原因无法进行横向扩容机器)

经过我们小组讨论,决定开发一个多数据源SDK,由我负责实现。让各个小组能够通过SDK直连需要的数据库,减少不必要的Dubbo调用。这个SDK不仅要给其他小组使用,我们自己也打算针对一些高频调用duboo接口替换为本地调用。

设计思路:条件化自动装配的多数据源SDK

我的设计目标是开发一个"智能"的SDK,能够根据配置自动装配所需的数据源、Dao和Service。业务方只需要引入依赖和添加配置,就可以直接使用相关的服务。

由于SDK中有些还需要包含一些业务逻辑,我们不能只提供DAO层,还需要提供Service层。为了避免与业务项目中可能已经存在的Bean出现名称冲突,所有Bean都加上了"Sdk"前缀

SDK项目结构设计

先来看看整个SDK的项目结构:

sdk-multi-datasource/
├── src/main/java/com/example/sdk/
│ ├── config/
│ │ ├── condition/
│ │ │ └── AnySdkDataSourceCondition.java
│ │ ├── datasource/
│ │ │ ├── SdkPrimaryDataConfig.java
│ │ │ └── SdkSecondaryDataConfig.java
│ │ └── SdkAutoConfiguration.java
│ ├── dao/
│ │ ├── primary/
│ │ │ └── SdkAppInfoDao.java
│ │ └── secondary/
│ │ └── SdkOtherDataDao.java
│ ├── service/
│ │ ├── SdkAppInfoService.java
│ │ └── SdkOtherDataService.java
│ ├── entity/
│ └── util/
├── src/main/resources/
│ ├── META-INF/
│ │ └── spring.factories
│ └── mapper/
│ ├── primary/
│ └── secondary/
└── pom.xml

核心代码实现

1. 条件判断类:智能感知数据源配置

首先,我创建了一个条件类,用于判断是否需要启用自动配置:

public class AnySdkDataSourceCondition implements Condition {
 @Override
 public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
 Environment env = context.getEnvironment();
 // 检查是否配置了任意一个SDK数据源
 // 条件注解的优势:只有业务方真正配置了数据源,SDK才会生效,避免不必要的Bean加载
 return env.containsProperty("spring.datasource.sdk-primary.jdbc-url") ||
 env.containsProperty("spring.datasource.sdk-secondary.jdbc-url");
 }
}

条件注解的优势在于它允许我们根据环境动态决定是否启用某些配置,这样可以避免加载不必要的Bean,提高应用启动速度,并且避免与业务项目中可能存在的Bean冲突。

2. 数据源配置:完整的SDK主数据源配置

下面是完整的主数据源配置代码,我添加了详细的注释说明:

@Configuration
// 条件注解:只有配置了sdk-primary数据源时才启用此配置
@ConditionalOnProperty(prefix = "spring.datasource.sdk-primary", name = "jdbc-url")
// 指定Mapper接口的扫描路径,并指定SqlSessionFactory的Bean名称
@MapperScan(
 basePackages = "com.example.sdk.dao.primary", 
 sqlSessionFactoryRef = "sdkPrimarySqlSessionFactory"
)
public class SdkPrimaryDataConfig {
 // 主数据源Bean,使用@ConfigurationProperties读取配置
 @Bean(name = "sdkPrimaryDataSource")
 @ConfigurationProperties(prefix = "spring.datasource.sdk-primary")
 public DataSource sdkPrimaryDataSource() {
 return DataSourceBuilder.create().build();
 }
 // 主数据源SqlSessionFactory
 @Bean(name = "sdkPrimarySqlSessionFactory")
 public SqlSessionFactory sdkPrimarySqlSessionFactory(
 @Qualifier("sdkPrimaryDataSource") DataSource dataSource) throws Exception {
 SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
 bean.setDataSource(dataSource);
 // 设置Mapper XML文件的位置
 bean.setMapperLocations(new PathMatchingResourcePatternResolver()
 .getResources("classpath*:mapper/primary/*.xml"));
 return bean.getObject();
 }
 // 主数据源SqlSessionTemplate
 @Bean(name = "sdkPrimarySqlSessionTemplate")
 public SqlSessionTemplate sdkPrimarySqlSessionTemplate(
 @Qualifier("sdkPrimarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
 return new SqlSessionTemplate(sqlSessionFactory);
 }
 // 主数据源事务管理器
 @Bean(name = "sdkPrimaryTransactionManager")
 public DataSourceTransactionManager sdkPrimaryTransactionManager(
 @Qualifier("sdkPrimaryDataSource") DataSource dataSource) {
 return new DataSourceTransactionManager(dataSource);
 }
}

次数据源配置SdkSecondaryDataConfig的结构与主数据源配置基本相同,区别在于:

  1. Bean名称中的"primary"替换为"secondary"
  2. 扫描的包路径不同(com.example.sdk.dao.secondary
  3. 配置前缀不同(spring.datasource.sdk-secondary

3. DAO层接口

为了避免与业务项目中的Bean冲突,所有DAO接口都加上了"Sdk"前缀:

@Mapper
public interface SdkAppInfoDao {
 AppInfo getByBusinessId(String businessId);
}

4. Service层实现

Service类也遵循相同的命名规则,为了保持SDK的简单性和灵活性,我选择了传统的setter注入方式:

public class SdkAppInfoService {
 private SdkAppInfoDao sdkAppInfoDao;
 public void setSdkAppInfoDao(SdkAppInfoDao sdkAppInfoDao) {
 this.sdkAppInfoDao = sdkAppInfoDao;
 }
 public AppInfo getByBusinessId(String businessId) {
 // 这里可以添加具体业务逻辑,如本地缓存、日志等
 return sdkAppInfoDao.getByBusinessId(businessId);
 }
}

5. 自动配置类:解决依赖注入问题

这是整个SDK的核心,我通过条件判断确保只有配置了对应数据源的情况下才创建相应的Service Bean:

@Configuration
@Conditional(AnySdkDataSourceCondition.class)
@Import({SdkPrimaryDataConfig.class, SdkSecondaryDataConfig.class})
public class SdkAutoConfiguration {
 // 只有配置了sdk-primary数据源时才创建此Bean
 @Bean
 @Lazy // 延迟加载,确保DAO先初始化
 @ConditionalOnProperty(prefix = "spring.datasource.sdk-primary", name = "jdbc-url")
 public SdkAppInfoService sdkAppInfoService(SdkAppInfoDao sdkAppInfoDao) {
 SdkAppInfoService service = new SdkAppInfoService();
 service.setSdkAppInfoDao(sdkAppInfoDao);
 return service;
 }
 
 // 只有配置了sdk-secondary数据源时才创建此Bean
 @Bean
 @Lazy
 @ConditionalOnProperty(prefix = "spring.datasource.sdk-secondary", name = "jdbc-url")
 public SdkOtherDataService sdkOtherDataService(SdkOtherDataDao sdkOtherDataDao) {
 SdkOtherDataService service = new SdkOtherDataService();
 service.setSdkOtherDataDao(sdkOtherDataDao);
 return service;
 }
}

这里使用了@Conditional(AnySdkDataSourceCondition.class)@ConditionalOnProperty注解,它的优势是能够根据配置文件中的属性值决定是否创建Bean。这样设计的好处是:

  1. 业务方未配置任何sdk数据源时,不会进行自动装配
  2. 只有在业务方真正配置了对应数据源时,才会创建相关的Service Bean
  3. 避免了不必要的Bean创建,减少内存占用
  4. 防止因缺少配置而导致的运行时错误

@Lazy 的核心作用是延迟 Bean 的初始化时机。在未使用该注解时,由于 Spring Bean 的创建顺序不确定,特别是在条件化配置中,Service 可能会在依赖的 Dao 之前被创建,导致注入的 Dao 实例为 null,进而引发异常。这本质上是由于 Bean 的依赖注入时机与初始化顺序不匹配所导致的。

通过添加 @Lazy,可以确保 Service 只有在首次被使用时才初始化,此时其依赖的 Dao 必然已经准备就绪,从而从根本上避免了顺序问题。

6. 注册自动配置

最后,在spring.factories中注册自动配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sdk.config.SdkAutoConfiguration

业务方使用方式

业务方使用我们这个SDK非常简单:

  1. 添加依赖
<dependency>
 <groupId>com.example</groupId>
 <artifactId>sdk-multi-datasource</artifactId>
 <version>1.0.0</version>
</dependency>
  1. 配置数据源(按照Spring Boot的配置习惯):
spring:
 datasource:
 sdk-primary:
 jdbc-url: jdbc:mysql://primary-db-host:3306/primary_db
 username: db_user
 password: db_password
 driver-class-name: com.mysql.jdbc.Driver
 sdk-secondary:
 jdbc-url: jdbc:mysql://secondary-db-host:3306/secondary_db
 username: db_user
 password: db_password
 driver-class-name: com.mysql.jdbc.Driver
  1. 直接使用Service
@RestController
public class BusinessController {
 
 @Autowired
 private SdkAppInfoService sdkAppInfoService;
 
 @GetMapping("/app-info/{businessId}")
 public AppInfo getAppInfo(@PathVariable String businessId) {
 return sdkAppInfoService.getByBusinessId(businessId);
 }
}

效果与反思

通过这个SDK,我们成功将部分高频的Dubbo调用改为了本地数据库直连,显著降低了延迟和系统负载。各个小组的反响也很好,他们喜欢这种"开箱即用"的体验。

条件注解的使用让我们的SDK更加智能和灵活:

  1. 按需加载:只有配置了数据源时才会加载相关Bean
  2. 避免冲突:通过条件判断和Bean命名约定,避免了与业务项目的Bean冲突
  3. 灵活配置:业务方可以根据需要选择启用哪些数据源

架构思考:微服务与单体的平衡

这个优化过程让我思考微服务架构与单体架构之间的平衡。微服务架构带来了清晰的服务边界和独立的扩展性,但也**引入了网络调用开销和分布式系统的复杂性。

通过这个多数据源SDK,我们找到了一种折中方案:既保持了微服务的架构优势,又在特定场景下获得了接近单体架构的性能

最重要的是根据实际场景选择最合适的方案。 在这个微服务大行其道的时代,偶尔回归"单体"思维,反而能让我们找到更好的平衡点。

从微服务到"部分单体",这不是倒退,而是架构思维的成熟。作为开发者,我们应该保持开放的心态,根据实际需求选择最合适的技术方案,而不是盲目追随技术潮流。

作者:爱学习的懒洋洋原文地址:https://www.cnblogs.com/xzqcsj/p/19250893

%s 个评论

要回复文章请先登录注册