Spring Boot自动装配实战:多数据源SDK解决Dubbo性能瓶颈
那些年我们学过的自动装配记得毕业那会刚开始学习Spring Boot的时候自动装配机制让我眼前一亮——约定大于配置的理念真是太巧妙了相信很多小伙伴都和我一样怀着好奇心去研究EnableAutoConfiguration和spring.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的结构与主数据源配置基本相同区别在于Bean名称中的primary替换为secondary扫描的包路径不同com.example.sdk.dao.secondary配置前缀不同spring.datasource.sdk-secondary3. 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 BeanConfiguration 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。这样设计的好处是业务方未配置任何sdk数据源时不会进行自动装配只有在业务方真正配置了对应数据源时才会创建相关的Service Bean避免了不必要的Bean创建减少内存占用防止因缺少配置而导致的运行时错误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非常简单添加依赖dependency groupIdcom.example/groupId artifactIdsdk-multi-datasource/artifactId version1.0.0/version /dependency配置数据源按照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