全部版块 我的主页
论坛 数据科学与人工智能 IT基础 JAVA语言开发
61 0
2025-11-24

在公司参与的一个大型项目中,系统采用了典型的微服务架构,各服务之间通过 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

SDK 的核心设计理念是“智能化”与“即插即用”。通过配置驱动的方式,自动完成数据源、DAO 层以及 Service 层的装配。使用者只需引入依赖并添加相应配置,即可直接使用封装好的服务逻辑。

考虑到部分功能需包含轻量级业务处理逻辑,仅提供 DAO 层不足以满足需求,因此 SDK 同时封装了 Service 层。为了防止与接入方项目中已存在的 Bean 发生命名冲突,所有 SDK 内部注册的 Bean 均统一加上 “Sdk” 前缀。

核心代码实现

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");
    }
}

利用 Spring 的条件注解机制,可以在运行时根据配置是否存在来决定是否创建相关组件。这种方式有效避免了无用 Bean 的加载,提升了启动效率,也减少了潜在的冲突风险。

2. 主数据源配置实现

以下是主数据源的完整配置代码,包含详细注释说明其作用和结构:

@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);
    }
}

3. 次数据源配置说明

次数据源的配置结构与主数据源基本一致,主要差异体现在以下几点:

  • Bean 名称中的 "primary" 替换为 "secondary"
  • 扫描的包路径不同
  • 使用的配置前缀不同
SdkSecondaryDataConfig
com.example.sdk.dao.secondary
spring.datasource.sdk-secondary

4. DAO 层接口定义

为确保与业务项目中的持久层组件不发生冲突,所有 DAO 接口均以 “Sdk” 作为前缀命名。

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

5. Service 层实现方案

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);
    }
}

6. 自动配置类:控制 Bean 创建的核心逻辑

这是整个 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 创建机制。这种设计带来了以下优势:

  • 当业务方未配置任何 SDK 数据源时,不会触发任何自动装配行为
  • 仅在实际配置存在时才初始化相关服务实例
  • 减少内存占用,避免资源浪费
  • 防止因缺失配置导致的运行时异常

此外,@Lazy 注解的引入解决了 Bean 初始化顺序问题。在默认情况下,Spring 容器中 Bean 的创建顺序不确定,可能导致 Service 在其所依赖的 DAO 尚未初始化完成时就被提前创建,造成注入失败。通过添加 @Lazy,Service 的初始化被延迟至首次调用时执行,此时其依赖项均已准备就绪,从根本上规避了此类问题。

7. 注册自动配置类

最后,在指定配置文件中注册自动配置类,使其能被 Spring Boot 正确识别并加载:

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

业务方接入方式

接入方使用该 SDK 的流程极为简便:

  1. 引入依赖:将 SDK 添加到项目依赖中
<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:无需手动装配,可直接注入并使用 SDK 提供的服务
@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 实现了高度智能化的装配能力:

  • 按需加载:仅在配置存在时激活对应功能模块
  • 避免冲突:通过命名隔离与条件控制,有效防止与宿主应用的 Bean 冲突
  • 灵活可选:支持按需启用主库或从库等不同数据源

架构层面的思考:微服务与性能之间的权衡

此次优化让我深入反思微服务架构与传统单体架构之间的取舍。虽然微服务提供了清晰的边界划分和独立伸缩能力,但也带来了额外的网络通信成本和分布式复杂性。

而通过这个多数据源 SDK,我们在保持微服务整体架构优势的前提下,针对特定高性能需求场景,实现了局部的“类单体”访问模式,获得了更优的性能表现。这实际上是一种务实的折中方案——既保留了解耦与可维护性,又在关键路径上规避了远程调用的开销。

在微服务盛行的当下,适时地回归“单体”设计思路,反而有助于我们找到系统架构中的更优平衡。关键在于结合具体业务场景,选择最契合的解决方案。

从全面拆分的微服务转向“部分单体”,并非技术上的倒退,而是架构理念走向成熟的体现。它反映了我们对复杂性与可维护性之间权衡的深入理解。

作为开发人员,应保持技术选型的灵活性与客观性,依据实际需求决定架构方向,避免盲目追逐流行趋势。真正优秀的系统,往往源于理性判断而非跟风选择。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群