在公司参与的一个大型项目中,系统采用了典型的微服务架构,各服务之间通过 Dubbo 实现远程调用。由于项目规模较大,开发工作被划分为多个小组,每个小组负责独立的微服务模块。
随着业务量持续增长,我们逐渐发现某些高频的数据查询操作在通过 Dubbo 调用时,已成为性能瓶颈。尽管单次调用延迟较低,但在高并发场景下,累积的网络开销显著增加。此外,提供 Dubbo 服务的一方因调用频率过高已接近并发极限,频繁触发告警,存在随时崩溃的风险。考虑到数据库本身配置较高,并非性能瓶颈所在,且受限于资源与部署策略,无法对 Dubbo 服务提供方进行横向扩容。
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
为解决这一问题,团队决定设计并实现一个“多数据源 SDK”,由我主导开发。该 SDK 的目标是让各小组能够绕过不必要的 Dubbo 调用,直接连接所需数据库,从而降低系统延迟和负载。同时,我们也将部分自身高频调用的接口切换为本地数据库访问方式。
SDK 的核心设计理念是“智能化”与“即插即用”。通过配置驱动的方式,自动完成数据源、DAO 层以及 Service 层的装配。使用者只需引入依赖并添加相应配置,即可直接使用封装好的服务逻辑。
考虑到部分功能需包含轻量级业务处理逻辑,仅提供 DAO 层不足以满足需求,因此 SDK 同时封装了 Service 层。为了防止与接入方项目中已存在的 Bean 发生命名冲突,所有 SDK 内部注册的 Bean 均统一加上 “Sdk” 前缀。
为实现按需加载,我设计了一个条件类,用于判断是否应启用对应的自动配置逻辑。
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");
}
}
利用 Spring 的条件注解机制,可以在运行时根据配置是否存在来决定是否创建相关组件。这种方式有效避免了无用 Bean 的加载,提升了启动效率,也减少了潜在的冲突风险。
以下是主数据源的完整配置代码,包含详细注释说明其作用和结构:
@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
com.example.sdk.dao.secondary
spring.datasource.sdk-secondary
为确保与业务项目中的持久层组件不发生冲突,所有 DAO 接口均以 “Sdk” 作为前缀命名。
@Mapper
public interface SdkAppInfoDao {
AppInfo getByBusinessId(String businessId);
}
Service 类同样遵循统一的命名规范。为了提升灵活性并简化依赖管理,采用传统的 setter 注入方式实现依赖注入。
public class SdkAppInfoService {
private SdkAppInfoDao sdkAppInfoDao;
public void setSdkAppInfoDao(SdkAppInfoDao sdkAppInfoDao) {
this.sdkAppInfoDao = sdkAppInfoDao;
}
public AppInfo getByBusinessId(String businessId) {
// 这里可以添加具体业务逻辑,如本地缓存、日志等
return sdkAppInfoDao.getByBusinessId(businessId);
}
}
这是整个 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 创建机制。这种设计带来了以下优势:
此外,@Lazy 注解的引入解决了 Bean 初始化顺序问题。在默认情况下,Spring 容器中 Bean 的创建顺序不确定,可能导致 Service 在其所依赖的 DAO 尚未初始化完成时就被提前创建,造成注入失败。通过添加 @Lazy,Service 的初始化被延迟至首次调用时执行,此时其依赖项均已准备就绪,从根本上规避了此类问题。
最后,在指定配置文件中注册自动配置类,使其能被 Spring Boot 正确识别并加载:
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sdk.config.SdkAutoConfiguration
接入方使用该 SDK 的流程极为简便:
<dependency>
<groupId>com.example</groupId>
<artifactId>sdk-multi-datasource</artifactId>
<version>1.0.0</version>
</dependency>
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
@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 实现了高度智能化的装配能力:
此次优化让我深入反思微服务架构与传统单体架构之间的取舍。虽然微服务提供了清晰的边界划分和独立伸缩能力,但也带来了额外的网络通信成本和分布式复杂性。
而通过这个多数据源 SDK,我们在保持微服务整体架构优势的前提下,针对特定高性能需求场景,实现了局部的“类单体”访问模式,获得了更优的性能表现。这实际上是一种务实的折中方案——既保留了解耦与可维护性,又在关键路径上规避了远程调用的开销。
在微服务盛行的当下,适时地回归“单体”设计思路,反而有助于我们找到系统架构中的更优平衡。关键在于结合具体业务场景,选择最契合的解决方案。
从全面拆分的微服务转向“部分单体”,并非技术上的倒退,而是架构理念走向成熟的体现。它反映了我们对复杂性与可维护性之间权衡的深入理解。
作为开发人员,应保持技术选型的灵活性与客观性,依据实际需求决定架构方向,避免盲目追逐流行趋势。真正优秀的系统,往往源于理性判断而非跟风选择。
扫码加好友,拉您进群



收藏
