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

明明学了自动装配,却鲜有机会实战?当我面对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的项目结构:

css 复制代码
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. 条件判断类:智能感知数据源配置

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

java 复制代码
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主数据源配置

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

java 复制代码
@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"前缀:

java 复制代码
@Mapper
public interface SdkAppInfoDao {
    AppInfo getByBusinessId(String businessId);
}

4. Service层实现

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

java 复制代码
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:

java 复制代码
@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中注册自动配置类:

properties 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sdk.config.SdkAutoConfiguration

业务方使用方式

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

  1. 添加依赖
xml 复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>sdk-multi-datasource</artifactId>
    <version>1.0.0</version>
</dependency>
  1. 配置数据源(按照Spring Boot的配置习惯):
yaml 复制代码
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
java 复制代码
@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,我们找到了一种折中方案:既保持了微服务的架构优势,又在特定场景下获得了接近单体架构的性能

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

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

相关推荐
四七伵5 小时前
为什么不推荐在 Java 项目中使用 java.util.Date?
java·后端
Json____5 小时前
使用springboot开发一个宿舍管理系统练习项目
java·spring boot·后端
爱读源码的大都督5 小时前
Java知名开源项目,5行代码,竟然有4个“bug”
java·后端·程序员
先做个垃圾出来………5 小时前
Pydantic库应用
java·数据库·python
编程毕设5 小时前
【含文档+PPT+源码】基于过滤协同算法的城市旅游网站的设计与实现
java·毕业设计·旅游·城市旅游
LogicArk5 小时前
Java八股文(一):基础篇
java
间彧5 小时前
Spring Boot项目中如何实现接口幂等
java
爱敲代码的TOM5 小时前
微服务基础2-网关路由
微服务·云原生·架构
失散135 小时前
分布式专题——14 RabbitMQ之集群实战
java·分布式·架构·rabbitmq