微火 發表於 2025-11-21 09:40:00

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

<blockquote>
<p>Spring文章专栏:https://juejin.cn/column/7511884538579877939</p>
</blockquote>
<blockquote>
<p>明明学了自动装配,却鲜有机会实战?当我面对Dubbo性能瓶颈时,一个自定义Starter的构想让我开启了Spring Boot条件化装配的奇妙之旅。</p>
</blockquote>
<h2 id="引言那些年我们学过的自动装配">引言:那些年我们学过的自动装配</h2>
<p>记得毕业那会刚开始学习Spring Boot的时候,<strong>自动装配机制</strong>让我眼前一亮——"<strong>约定大于配置</strong>"的理念真是太巧妙了!相信很多小伙伴都和我一样,怀着好奇心去研究<code>@EnableAutoConfiguration</code>和<code>spring.factories</code>的奥秘,甚至动手尝试编写过自己的Starter。</p>
<p>但说实话,在实际项目开发中,真正需要自己实现自动装配的场景并不多。大多数时候,我们都是在使用Spring Boot官方或者第三方提供的Starter。直到最近,我遇到了一个实实在在的需求,才让我有机会深入实践这个机制。</p>
<h2 id="背景dubbo调用成了性能瓶颈">背景:Dubbo调用成了性能瓶颈</h2>
<p>我在公司参与的这个大型项目采用了典型的<strong>微服务架构</strong>,各个服务之间通过Dubbo进行调用。项目规模较大,因此分成多个开发小组,每个小组负责不同的微服务模块。</p>
<p>随着业务量增长,我们发现了一个棘手的问题:<strong>某些高频的数据查询操作通过Dubbo调用时,性能开销变得不可忽视</strong>。虽然单次调用的延迟不大,但在高并发场景下,这些开销累积起来就相当可观了。同时<strong>提供duboo的服务,因为高频调用已经存在并发瓶颈,频繁告警,如果继续增加调用量随时可能崩溃</strong>。(因为数据库规格较高,瓶颈不在于数据库,而只在于dubbo服务提供方,且因为各种原因无法进行横向扩容机器)</p>
<p>经过我们小组讨论,决定开发一个<strong>多数据源SDK</strong>,由我负责实现。让各个小组能够通过SDK直连需要的数据库,减少不必要的Dubbo调用。这个SDK不仅要给其他小组使用,我们自己也打算针对一些高频调用duboo接口替换为本地调用。</p>
<h2 id="设计思路条件化自动装配的多数据源sdk">设计思路:条件化自动装配的多数据源SDK</h2>
<p>我的设计目标是开发一个"智能"的SDK,能够根据配置自动装配所需的数据源、Dao和Service。业务方只需要引入依赖和添加配置,就可以直接使用相关的服务。</p>
<p>由于SDK中有些还需要包含一些业务逻辑,我们不能只提供DAO层,还需要提供Service层。<strong>为了避免与业务项目中可能已经存在的Bean出现名称冲突,所有Bean都加上了"Sdk"前缀</strong>。</p>
<h3 id="sdk项目结构设计">SDK项目结构设计</h3>
<p>先来看看整个SDK的项目结构:</p>
<pre><code>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
</code></pre>
<h3 id="核心代码实现">核心代码实现</h3>
<h4 id="1-条件判断类智能感知数据源配置">1. 条件判断类:智能感知数据源配置</h4>
<p>首先,我创建了一个条件类,用于判断是否需要启用自动配置:</p>
<pre><code class="language-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");
    }
}
</code></pre>
<blockquote>
<p><strong>条件注解</strong>的优势在于它允许我们根据环境<strong>动态决定是否启用某些配置</strong>,这样可以避免加载不必要的Bean,提高应用启动速度,并且避免与业务项目中可能存在的Bean冲突。</p>
</blockquote>
<h4 id="2-数据源配置完整的sdk主数据源配置">2. 数据源配置:完整的SDK主数据源配置</h4>
<p>下面是完整的主数据源配置代码,我添加了详细的注释说明:</p>
<pre><code class="language-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);
    }
}
</code></pre>
<blockquote>
<p>次数据源配置<code>SdkSecondaryDataConfig</code>的结构与主数据源配置基本相同,区别在于:</p>
<ol>
<li>Bean名称中的"primary"替换为"secondary"</li>
<li>扫描的包路径不同(<code>com.example.sdk.dao.secondary</code>)</li>
<li>配置前缀不同(<code>spring.datasource.sdk-secondary</code>)</li>
</ol>
</blockquote>
<h4 id="3-dao层接口">3. DAO层接口</h4>
<p>为了避免与业务项目中的Bean冲突,所有DAO接口都加上了"Sdk"前缀:</p>
<pre><code class="language-java">@Mapper
public interface SdkAppInfoDao {
    AppInfo getByBusinessId(String businessId);
}
</code></pre>
<h4 id="4-service层实现">4. Service层实现</h4>
<p>Service类也遵循相同的命名规则,为了保持SDK的简单性和灵活性,我选择了传统的setter注入方式:</p>
<pre><code class="language-java">public class SdkAppInfoService {
    private SdkAppInfoDao sdkAppInfoDao;

    public void setSdkAppInfoDao(SdkAppInfoDao sdkAppInfoDao) {
      this.sdkAppInfoDao = sdkAppInfoDao;
    }

    public AppInfo getByBusinessId(String businessId) {
      // 这里可以添加具体业务逻辑,如本地缓存、日志等
      return sdkAppInfoDao.getByBusinessId(businessId);
    }
}
</code></pre>
<h4 id="5-自动配置类解决依赖注入问题">5. 自动配置类:解决依赖注入问题</h4>
<p>这是整个<strong>SDK的核心</strong>,我通过条件判断确保只有配置了对应数据源的情况下才创建相应的Service Bean:</p>
<pre><code class="language-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;
    }
}
</code></pre>
<p>这里使用了<code>@Conditional(AnySdkDataSourceCondition.class)</code>和<code>@ConditionalOnProperty</code>注解,它的优势是能够根据配置文件中的属性值决定是否创建Bean。这样设计的好处是:</p>
<ol>
<li>业务方未配置任何sdk数据源时,不会进行自动装配</li>
<li>只有在业务方真正配置了对应数据源时,才会创建相关的Service Bean</li>
<li>避免了不必要的Bean创建,减少内存占用</li>
<li>防止因缺少配置而导致的运行时错误</li>
</ol>
<blockquote>
<p><strong>@Lazy 的核心作用是延迟 Bean 的初始化时机</strong>。在未使用该注解时,由于 Spring Bean 的创建顺序不确定,特别是在条件化配置中,Service 可能会在依赖的 Dao 之前被创建,导致注入的 Dao 实例为 null,进而引发异常。这本质上是由于 Bean 的依赖注入时机与初始化顺序不匹配所导致的。</p>
<p>通过添加 @Lazy,可以确保 Service 只有在首次被使用时才初始化,此时其依赖的 Dao 必然已经准备就绪,从而从根本上避免了顺序问题。</p>
</blockquote>
<h4 id="6-注册自动配置">6. 注册自动配置</h4>
<p>最后,在<code>spring.factories</code>中注册自动配置类:</p>
<pre><code class="language-properties">org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sdk.config.SdkAutoConfiguration
</code></pre>
<h2 id="业务方使用方式">业务方使用方式</h2>
<p>业务方使用我们这个SDK非常简单:</p>
<ol>
<li><strong>添加依赖</strong>:</li>
</ol>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;com.example&lt;/groupId&gt;
    &lt;artifactId&gt;sdk-multi-datasource&lt;/artifactId&gt;
    &lt;version&gt;1.0.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<ol start="2">
<li><strong>配置数据源</strong>(按照Spring Boot的配置习惯):</li>
</ol>
<pre><code class="language-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
</code></pre>
<ol start="3">
<li><strong>直接使用Service</strong>:</li>
</ol>
<pre><code class="language-java">@RestController
public class BusinessController {
   
    @Autowired
    private SdkAppInfoService sdkAppInfoService;
   
    @GetMapping("/app-info/{businessId}")
    public AppInfo getAppInfo(@PathVariable String businessId) {
      return sdkAppInfoService.getByBusinessId(businessId);
    }
}
</code></pre>
<h2 id="效果与反思">效果与反思</h2>
<p>通过这个SDK,我们成功将部分高频的Dubbo调用改为了本地数据库直连,显著降低了延迟和系统负载。各个小组的反响也很好,他们喜欢这种"开箱即用"的体验。</p>
<p>条件注解的使用让我们的SDK更加智能和灵活:</p>
<ol>
<li><strong>按需加载</strong>:只有配置了数据源时才会加载相关Bean</li>
<li><strong>避免冲突</strong>:通过条件判断和Bean命名约定,避免了与业务项目的Bean冲突</li>
<li><strong>灵活配置</strong>:业务方可以根据需要选择启用哪些数据源</li>
</ol>
<h2 id="架构思考微服务与单体的平衡">架构思考:微服务与单体的平衡</h2>
<p>这个优化过程让我思考微服务架构与单体架构之间的平衡。微服务架构带来了<strong>清晰的服务边界和独立的扩展性</strong>,但也**引入了网络调用开销和分布式系统的复杂性。</p>
<p>通过这个多数据源SDK,我们找到了一种折中方案:<strong>既保持了微服务的架构优势,又在特定场景下获得了接近单体架构的性能</strong>。</p>
<p>最重要的是根据实际场景选择最合适的方案。 在这个微服务大行其道的时代,偶尔回归"单体"思维,反而能让我们找到更好的平衡点。</p>
<p>从微服务到"部分单体",这不是倒退,而是架构思维的成熟。作为开发者,我们应该保持开放的心态,根据实际需求选择最合适的技术方案,而不是盲目追随技术潮流。</p>
<hr>
<blockquote>
<p>文章的最后,想和你多聊两句。</p>
<p>技术之路,常常是热闹与孤独并存。那些深夜的调试、灵光一闪的方案、还有踩坑爬起后的顿悟,如果能有人一起聊聊,该多好。</p>
<p>为此,我建了一个小花园——我的微信公众号「<strong>[努力的小郑]</strong>」。</p>
<p>这里没有高深莫测的理论堆砌,只有我对后端开发、系统设计和工程实践的持续思考与沉淀。它更像我的<strong>数字笔记本</strong>,记录着那些值得被记住的解决方案和思维火花。</p>
<p>如果你觉得今天的文章还有一点启发,或者单纯想找一个同行者偶尔聊聊技术、谈谈思考,那么,欢迎你来坐坐。<br>
<img src="https://img2024.cnblogs.com/blog/3703499/202601/3703499-20260105210259813-964799315.jpg"></p>
<p>愿你前行路上,总有代码可写,有梦可追,也有灯火可亲。</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/xzqcsj/p/19250893
頁: [1]
查看完整版本: Spring Boot自动装配实战:多数据源SDK解决Dubbo性能瓶颈