SpringBoot自動裝配之多數(shù)據(jù)源SDK解決Dubbo性能瓶頸詳解
明明學(xué)了自動裝配,卻鮮有機會實戰(zhàn)?當(dāng)我面對Dubbo性能瓶頸時,一個自定義Starter的構(gòu)想讓我開啟了Spring Boot條件化裝配的奇妙之旅。
引言:那些年我們學(xué)過的自動裝配
記得畢業(yè)那會剛開始學(xué)習(xí)Spring Boot的時候,自動裝配機制讓我眼前一亮——"約定大于配置"的理念真是太巧妙了!相信很多小伙伴都和我一樣,懷著好奇心去研究@EnableAutoConfiguration和spring.factories的奧秘,甚至動手嘗試編寫過自己的Starter。
但說實話,在實際項目開發(fā)中,真正需要自己實現(xiàn)自動裝配的場景并不多。大多數(shù)時候,我們都是在使用Spring Boot官方或者第三方提供的Starter。直到最近,我遇到了一個實實在在的需求,才讓我有機會深入實踐這個機制。
背景:Dubbo調(diào)用成了性能瓶頸
我在公司參與的這個大型項目采用了典型的微服務(wù)架構(gòu),各個服務(wù)之間通過Dubbo進行調(diào)用。項目規(guī)模較大,因此分成多個開發(fā)小組,每個小組負(fù)責(zé)不同的微服務(wù)模塊。
隨著業(yè)務(wù)量增長,我們發(fā)現(xiàn)了一個棘手的問題:某些高頻的數(shù)據(jù)查詢操作通過Dubbo調(diào)用時,性能開銷變得不可忽視。雖然單次調(diào)用的延遲不大,但在高并發(fā)場景下,這些開銷累積起來就相當(dāng)可觀了。同時提供duboo的服務(wù),因為高頻調(diào)用已經(jīng)存在并發(fā)瓶頸,頻繁告警,如果繼續(xù)增加調(diào)用量隨時可能崩潰。(因為數(shù)據(jù)庫規(guī)格較高,瓶頸不在于數(shù)據(jù)庫,而只在于dubbo服務(wù)提供方,且因為各種原因無法進行橫向擴容機器)
經(jīng)過我們小組討論,決定開發(fā)一個多數(shù)據(jù)源SDK,由我負(fù)責(zé)實現(xiàn)。讓各個小組能夠通過SDK直連需要的數(shù)據(jù)庫,減少不必要的Dubbo調(diào)用。這個SDK不僅要給其他小組使用,我們自己也打算針對一些高頻調(diào)用duboo接口替換為本地調(diào)用。
設(shè)計思路:條件化自動裝配的多數(shù)據(jù)源SDK
我的設(shè)計目標(biāo)是開發(fā)一個"智能"的SDK,能夠根據(jù)配置自動裝配所需的數(shù)據(jù)源、Dao和Service。業(yè)務(wù)方只需要引入依賴和添加配置,就可以直接使用相關(guān)的服務(wù)。
由于SDK中有些還需要包含一些業(yè)務(wù)邏輯,我們不能只提供DAO層,還需要提供Service層。為了避免與業(yè)務(wù)項目中可能已經(jīng)存在的Bean出現(xiàn)名稱沖突,所有Bean都加上了"Sdk"前綴。
SDK項目結(jié)構(gòu)設(shè)計
先來看看整個SDK的項目結(jié)構(gòu):
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
核心代碼實現(xiàn)
1. 條件判斷類:智能感知數(shù)據(jù)源配置
首先,我創(chuàng)建了一個條件類,用于判斷是否需要啟用自動配置:
public class AnySdkDataSourceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
// 檢查是否配置了任意一個SDK數(shù)據(jù)源
// 條件注解的優(yōu)勢:只有業(yè)務(wù)方真正配置了數(shù)據(jù)源,SDK才會生效,避免不必要的Bean加載
return env.containsProperty("spring.datasource.sdk-primary.jdbc-url") ||
env.containsProperty("spring.datasource.sdk-secondary.jdbc-url");
}
}
條件注解的優(yōu)勢在于它允許我們根據(jù)環(huán)境動態(tài)決定是否啟用某些配置,這樣可以避免加載不必要的Bean,提高應(yīng)用啟動速度,并且避免與業(yè)務(wù)項目中可能存在的Bean沖突。
2. 數(shù)據(jù)源配置:完整的SDK主數(shù)據(jù)源配置
下面是完整的主數(shù)據(jù)源配置代碼,我添加了詳細的注釋說明:
@Configuration
// 條件注解:只有配置了sdk-primary數(shù)據(jù)源時才啟用此配置
@ConditionalOnProperty(prefix = "spring.datasource.sdk-primary", name = "jdbc-url")
// 指定Mapper接口的掃描路徑,并指定SqlSessionFactory的Bean名稱
@MapperScan(
basePackages = "com.example.sdk.dao.primary",
sqlSessionFactoryRef = "sdkPrimarySqlSessionFactory"
)
public class SdkPrimaryDataConfig {
// 主數(shù)據(jù)源Bean,使用@ConfigurationProperties讀取配置
@Bean(name = "sdkPrimaryDataSource")
@ConfigurationProperties(prefix = "spring.datasource.sdk-primary")
public DataSource sdkPrimaryDataSource() {
return DataSourceBuilder.create().build();
}
// 主數(shù)據(jù)源SqlSessionFactory
@Bean(name = "sdkPrimarySqlSessionFactory")
public SqlSessionFactory sdkPrimarySqlSessionFactory(
@Qualifier("sdkPrimaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// 設(shè)置Mapper XML文件的位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/primary/*.xml"));
return bean.getObject();
}
// 主數(shù)據(jù)源SqlSessionTemplate
@Bean(name = "sdkPrimarySqlSessionTemplate")
public SqlSessionTemplate sdkPrimarySqlSessionTemplate(
@Qualifier("sdkPrimarySqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
// 主數(shù)據(jù)源事務(wù)管理器
@Bean(name = "sdkPrimaryTransactionManager")
public DataSourceTransactionManager sdkPrimaryTransactionManager(
@Qualifier("sdkPrimaryDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
次數(shù)據(jù)源配置SdkSecondaryDataConfig的結(jié)構(gòu)與主數(shù)據(jù)源配置基本相同,區(qū)別在于:
- Bean名稱中的"primary"替換為"secondary"
- 掃描的包路徑不同(
com.example.sdk.dao.secondary) - 配置前綴不同(
spring.datasource.sdk-secondary)
3. DAO層接口
為了避免與業(yè)務(wù)項目中的Bean沖突,所有DAO接口都加上了"Sdk"前綴:
@Mapper
public interface SdkAppInfoDao {
AppInfo getByBusinessId(String businessId);
}
4. Service層實現(xiàn)
Service類也遵循相同的命名規(guī)則,為了保持SDK的簡單性和靈活性,我選擇了傳統(tǒng)的setter注入方式:
public class SdkAppInfoService {
private SdkAppInfoDao sdkAppInfoDao;
public void setSdkAppInfoDao(SdkAppInfoDao sdkAppInfoDao) {
this.sdkAppInfoDao = sdkAppInfoDao;
}
public AppInfo getByBusinessId(String businessId) {
// 這里可以添加具體業(yè)務(wù)邏輯,如本地緩存、日志等
return sdkAppInfoDao.getByBusinessId(businessId);
}
}
5. 自動配置類:解決依賴注入問題
這是整個SDK的核心,我通過條件判斷確保只有配置了對應(yīng)數(shù)據(jù)源的情況下才創(chuàng)建相應(yīng)的Service Bean:
@Configuration
@Conditional(AnySdkDataSourceCondition.class)
@Import({SdkPrimaryDataConfig.class, SdkSecondaryDataConfig.class})
public class SdkAutoConfiguration {
// 只有配置了sdk-primary數(shù)據(jù)源時才創(chuàng)建此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數(shù)據(jù)源時才創(chuàng)建此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注解,它的優(yōu)勢是能夠根據(jù)配置文件中的屬性值決定是否創(chuàng)建Bean。這樣設(shè)計的好處是:
- 業(yè)務(wù)方未配置任何sdk數(shù)據(jù)源時,不會進行自動裝配
- 只有在業(yè)務(wù)方真正配置了對應(yīng)數(shù)據(jù)源時,才會創(chuàng)建相關(guān)的Service Bean
- 避免了不必要的Bean創(chuàng)建,減少內(nèi)存占用
- 防止因缺少配置而導(dǎo)致的運行時錯誤
@Lazy 的核心作用是延遲 Bean 的初始化時機。在未使用該注解時,由于 Spring Bean 的創(chuàng)建順序不確定,特別是在條件化配置中,Service 可能會在依賴的 Dao 之前被創(chuàng)建,導(dǎo)致注入的 Dao 實例為 null,進而引發(fā)異常。這本質(zhì)上是由于 Bean 的依賴注入時機與初始化順序不匹配所導(dǎo)致的。
通過添加 @Lazy,可以確保 Service 只有在首次被使用時才初始化,此時其依賴的 Dao 必然已經(jīng)準(zhǔn)備就緒,從而從根本上避免了順序問題。
6. 注冊自動配置
最后,在spring.factories中注冊自動配置類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.sdk.config.SdkAutoConfiguration
業(yè)務(wù)方使用方式
業(yè)務(wù)方使用我們這個SDK非常簡單:
添加依賴:
<dependency>
<groupId>com.example</groupId>
<artifactId>sdk-multi-datasource</artifactId>
<version>1.0.0</version>
</dependency>
配置數(shù)據(jù)源(按照Spring Boot的配置習(xí)慣):
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
直接使用Service:
@RestController
public class BusinessController {
@Autowired
private SdkAppInfoService sdkAppInfoService;
@GetMapping("/app-info/{businessId}")
public AppInfo getAppInfo(@PathVariable String businessId) {
return sdkAppInfoService.getByBusinessId(businessId);
}
}
效果與反思
通過這個SDK,我們成功將部分高頻的Dubbo調(diào)用改為了本地數(shù)據(jù)庫直連,顯著降低了延遲和系統(tǒng)負(fù)載。各個小組的反響也很好,他們喜歡這種"開箱即用"的體驗。
條件注解的使用讓我們的SDK更加智能和靈活:
- 按需加載:只有配置了數(shù)據(jù)源時才會加載相關(guān)Bean
- 避免沖突:通過條件判斷和Bean命名約定,避免了與業(yè)務(wù)項目的Bean沖突
- 靈活配置:業(yè)務(wù)方可以根據(jù)需要選擇啟用哪些數(shù)據(jù)源
架構(gòu)思考:微服務(wù)與單體的平衡
這個優(yōu)化過程讓我思考微服務(wù)架構(gòu)與單體架構(gòu)之間的平衡。微服務(wù)架構(gòu)帶來了清晰的服務(wù)邊界和獨立的擴展性,但也**引入了網(wǎng)絡(luò)調(diào)用開銷和分布式系統(tǒng)的復(fù)雜性。
通過這個多數(shù)據(jù)源SDK,我們找到了一種折中方案:既保持了微服務(wù)的架構(gòu)優(yōu)勢,又在特定場景下獲得了接近單體架構(gòu)的性能。
最重要的是根據(jù)實際場景選擇最合適的方案。 在這個微服務(wù)大行其道的時代,偶爾回歸"單體"思維,反而能讓我們找到更好的平衡點。
從微服務(wù)到"部分單體",這不是倒退,而是架構(gòu)思維的成熟。作為開發(fā)者,我們應(yīng)該保持開放的心態(tài),根據(jù)實際需求選擇最合適的技術(shù)方案,而不是盲目追隨技術(shù)潮流。
到此這篇關(guān)于SpringBoot自動裝配之多數(shù)據(jù)源SDK解決Dubbo性能瓶頸詳解的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)源SDK優(yōu)化Dubbo性能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloudAlibaba分布式配置中心Nacos持久化方式
文章介紹了如何創(chuàng)建Nacos數(shù)據(jù)庫并初始化,修改Nacos配置文件以通過數(shù)據(jù)庫動態(tài)修改配置文件,作者分享了遇到的問題和解決方法,提醒在重啟服務(wù)器時注意MySQL是否已成功啟動2026-01-01
Mybatis傳遞多個參數(shù)的三種實現(xiàn)方法
這篇文章主要介紹了Mybatis傳遞多個參數(shù)的三種實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-04-04
HttpClient POST請求第三方接口問題(多參數(shù)傳參)
這篇文章主要介紹了HttpClient POST請求第三方接口問題(多參數(shù)傳參),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot與Redis的令牌主動失效機制實現(xiàn)
本文詳細介紹了基于SpringBoot和Redis實現(xiàn)令牌主動失效機制,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12
SpringBoot-RestTemplate實現(xiàn)調(diào)用第三方API的方式
RestTemplate?是由?Spring?提供的一個?HTTP?請求工具,它提供了常見的REST請求方案的模版,例如?GET?請求、POST?請求、PUT?請求、DELETE?請求以及一些通用的請求執(zhí)行方法?exchange?以及?execute,下面看下SpringBoot?RestTemplate調(diào)用第三方API的方式2022-12-12
springboot集成nacos無法動態(tài)獲取nacos配置的問題
這篇文章主要介紹了springboot集成nacos無法動態(tài)獲取nacos配置的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09

