SpringBoot自動(dòng)裝配之多數(shù)據(jù)源SDK解決Dubbo性能瓶頸詳解
明明學(xué)了自動(dòng)裝配,卻鮮有機(jī)會(huì)實(shí)戰(zhàn)?當(dāng)我面對(duì)Dubbo性能瓶頸時(shí),一個(gè)自定義Starter的構(gòu)想讓我開啟了Spring Boot條件化裝配的奇妙之旅。
引言:那些年我們學(xué)過的自動(dòng)裝配
記得畢業(yè)那會(huì)剛開始學(xué)習(xí)Spring Boot的時(shí)候,自動(dòng)裝配機(jī)制讓我眼前一亮——"約定大于配置"的理念真是太巧妙了!相信很多小伙伴都和我一樣,懷著好奇心去研究@EnableAutoConfiguration和spring.factories的奧秘,甚至動(dòng)手嘗試編寫過自己的Starter。
但說實(shí)話,在實(shí)際項(xiàng)目開發(fā)中,真正需要自己實(shí)現(xiàn)自動(dòng)裝配的場(chǎng)景并不多。大多數(shù)時(shí)候,我們都是在使用Spring Boot官方或者第三方提供的Starter。直到最近,我遇到了一個(gè)實(shí)實(shí)在在的需求,才讓我有機(jī)會(huì)深入實(shí)踐這個(gè)機(jī)制。
背景:Dubbo調(diào)用成了性能瓶頸
我在公司參與的這個(gè)大型項(xiàng)目采用了典型的微服務(wù)架構(gòu),各個(gè)服務(wù)之間通過Dubbo進(jìn)行調(diào)用。項(xiàng)目規(guī)模較大,因此分成多個(gè)開發(fā)小組,每個(gè)小組負(fù)責(zé)不同的微服務(wù)模塊。
隨著業(yè)務(wù)量增長,我們發(fā)現(xiàn)了一個(gè)棘手的問題:某些高頻的數(shù)據(jù)查詢操作通過Dubbo調(diào)用時(shí),性能開銷變得不可忽視。雖然單次調(diào)用的延遲不大,但在高并發(fā)場(chǎng)景下,這些開銷累積起來就相當(dāng)可觀了。同時(shí)提供duboo的服務(wù),因?yàn)楦哳l調(diào)用已經(jīng)存在并發(fā)瓶頸,頻繁告警,如果繼續(xù)增加調(diào)用量隨時(shí)可能崩潰。(因?yàn)閿?shù)據(jù)庫規(guī)格較高,瓶頸不在于數(shù)據(jù)庫,而只在于dubbo服務(wù)提供方,且因?yàn)楦鞣N原因無法進(jìn)行橫向擴(kuò)容機(jī)器)
經(jīng)過我們小組討論,決定開發(fā)一個(gè)多數(shù)據(jù)源SDK,由我負(fù)責(zé)實(shí)現(xiàn)。讓各個(gè)小組能夠通過SDK直連需要的數(shù)據(jù)庫,減少不必要的Dubbo調(diào)用。這個(gè)SDK不僅要給其他小組使用,我們自己也打算針對(duì)一些高頻調(diào)用duboo接口替換為本地調(diào)用。
設(shè)計(jì)思路:條件化自動(dòng)裝配的多數(shù)據(jù)源SDK
我的設(shè)計(jì)目標(biāo)是開發(fā)一個(gè)"智能"的SDK,能夠根據(jù)配置自動(dòng)裝配所需的數(shù)據(jù)源、Dao和Service。業(yè)務(wù)方只需要引入依賴和添加配置,就可以直接使用相關(guān)的服務(wù)。
由于SDK中有些還需要包含一些業(yè)務(wù)邏輯,我們不能只提供DAO層,還需要提供Service層。為了避免與業(yè)務(wù)項(xiàng)目中可能已經(jīng)存在的Bean出現(xiàn)名稱沖突,所有Bean都加上了"Sdk"前綴。
SDK項(xiàng)目結(jié)構(gòu)設(shè)計(jì)
先來看看整個(gè)SDK的項(xiàng)目結(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
核心代碼實(shí)現(xiàn)
1. 條件判斷類:智能感知數(shù)據(jù)源配置
首先,我創(chuàng)建了一個(gè)條件類,用于判斷是否需要啟用自動(dòng)配置:
public class AnySdkDataSourceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
// 檢查是否配置了任意一個(gè)SDK數(shù)據(jù)源
// 條件注解的優(yōu)勢(shì):只有業(yè)務(wù)方真正配置了數(shù)據(jù)源,SDK才會(huì)生效,避免不必要的Bean加載
return env.containsProperty("spring.datasource.sdk-primary.jdbc-url") ||
env.containsProperty("spring.datasource.sdk-secondary.jdbc-url");
}
}
條件注解的優(yōu)勢(shì)在于它允許我們根據(jù)環(huán)境動(dòng)態(tài)決定是否啟用某些配置,這樣可以避免加載不必要的Bean,提高應(yīng)用啟動(dòng)速度,并且避免與業(yè)務(wù)項(xiàng)目中可能存在的Bean沖突。
2. 數(shù)據(jù)源配置:完整的SDK主數(shù)據(jù)源配置
下面是完整的主數(shù)據(jù)源配置代碼,我添加了詳細(xì)的注釋說明:
@Configuration
// 條件注解:只有配置了sdk-primary數(shù)據(jù)源時(shí)才啟用此配置
@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ù)項(xiàng)目中的Bean沖突,所有DAO接口都加上了"Sdk"前綴:
@Mapper
public interface SdkAppInfoDao {
AppInfo getByBusinessId(String businessId);
}
4. Service層實(shí)現(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. 自動(dòng)配置類:解決依賴注入問題
這是整個(gè)SDK的核心,我通過條件判斷確保只有配置了對(duì)應(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ù)源時(shí)才創(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ù)源時(shí)才創(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)勢(shì)是能夠根據(jù)配置文件中的屬性值決定是否創(chuàng)建Bean。這樣設(shè)計(jì)的好處是:
- 業(yè)務(wù)方未配置任何sdk數(shù)據(jù)源時(shí),不會(huì)進(jìn)行自動(dòng)裝配
- 只有在業(yè)務(wù)方真正配置了對(duì)應(yīng)數(shù)據(jù)源時(shí),才會(huì)創(chuàng)建相關(guān)的Service Bean
- 避免了不必要的Bean創(chuàng)建,減少內(nèi)存占用
- 防止因缺少配置而導(dǎo)致的運(yùn)行時(shí)錯(cuò)誤
@Lazy 的核心作用是延遲 Bean 的初始化時(shí)機(jī)。在未使用該注解時(shí),由于 Spring Bean 的創(chuàng)建順序不確定,特別是在條件化配置中,Service 可能會(huì)在依賴的 Dao 之前被創(chuàng)建,導(dǎo)致注入的 Dao 實(shí)例為 null,進(jìn)而引發(fā)異常。這本質(zhì)上是由于 Bean 的依賴注入時(shí)機(jī)與初始化順序不匹配所導(dǎo)致的。
通過添加 @Lazy,可以確保 Service 只有在首次被使用時(shí)才初始化,此時(shí)其依賴的 Dao 必然已經(jīng)準(zhǔn)備就緒,從而從根本上避免了順序問題。
6. 注冊(cè)自動(dòng)配置
最后,在spring.factories中注冊(cè)自動(dòng)配置類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.sdk.config.SdkAutoConfiguration
業(yè)務(wù)方使用方式
業(yè)務(wù)方使用我們這個(gè)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);
}
}
效果與反思
通過這個(gè)SDK,我們成功將部分高頻的Dubbo調(diào)用改為了本地?cái)?shù)據(jù)庫直連,顯著降低了延遲和系統(tǒng)負(fù)載。各個(gè)小組的反響也很好,他們喜歡這種"開箱即用"的體驗(yàn)。
條件注解的使用讓我們的SDK更加智能和靈活:
- 按需加載:只有配置了數(shù)據(jù)源時(shí)才會(huì)加載相關(guān)Bean
- 避免沖突:通過條件判斷和Bean命名約定,避免了與業(yè)務(wù)項(xiàng)目的Bean沖突
- 靈活配置:業(yè)務(wù)方可以根據(jù)需要選擇啟用哪些數(shù)據(jù)源
架構(gòu)思考:微服務(wù)與單體的平衡
這個(gè)優(yōu)化過程讓我思考微服務(wù)架構(gòu)與單體架構(gòu)之間的平衡。微服務(wù)架構(gòu)帶來了清晰的服務(wù)邊界和獨(dú)立的擴(kuò)展性,但也**引入了網(wǎng)絡(luò)調(diào)用開銷和分布式系統(tǒng)的復(fù)雜性。
通過這個(gè)多數(shù)據(jù)源SDK,我們找到了一種折中方案:既保持了微服務(wù)的架構(gòu)優(yōu)勢(shì),又在特定場(chǎng)景下獲得了接近單體架構(gòu)的性能。
最重要的是根據(jù)實(shí)際場(chǎng)景選擇最合適的方案。 在這個(gè)微服務(wù)大行其道的時(shí)代,偶爾回歸"單體"思維,反而能讓我們找到更好的平衡點(diǎn)。
從微服務(wù)到"部分單體",這不是倒退,而是架構(gòu)思維的成熟。作為開發(fā)者,我們應(yīng)該保持開放的心態(tài),根據(jù)實(shí)際需求選擇最合適的技術(shù)方案,而不是盲目追隨技術(shù)潮流。
到此這篇關(guān)于SpringBoot自動(dòng)裝配之多數(shù)據(jù)源SDK解決Dubbo性能瓶頸詳解的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)源SDK優(yōu)化Dubbo性能內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
提升網(wǎng)絡(luò)請(qǐng)求穩(wěn)定性HttpClient的重試機(jī)制深入理解
這篇文章主要為大家介紹了提升網(wǎng)絡(luò)請(qǐng)求穩(wěn)定性HttpClient的重試機(jī)制深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
SpringBoot中整合knife4j接口文檔的實(shí)踐
這篇文章主要介紹了SpringBoot中整合knife4j接口文檔的實(shí)踐,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
SpringBoot Starter機(jī)制及整合tomcat的實(shí)現(xiàn)詳解
這篇文章主要介紹了SpringBoot Starter機(jī)制及整合tomcat的實(shí)現(xiàn),我們知道SpringBoot自己在“后臺(tái)”幫我們配置了很多原本需要我們手動(dòng)去的東西,至于這個(gè)“后臺(tái)”是啥,就是Starter機(jī)制2022-09-09
關(guān)于Javaweb的轉(zhuǎn)發(fā)和重定向詳解
這篇文章主要介紹了關(guān)于Javaweb的轉(zhuǎn)發(fā)和重定向詳解,請(qǐng)求的轉(zhuǎn)發(fā),是指服務(wù)器收到請(qǐng)求后,從一個(gè)服務(wù)器端資源跳轉(zhuǎn)到同一個(gè)服務(wù)器端另外一個(gè)資源的操作,需要的朋友可以參考下2023-05-05
Java使用BigDecimal公式精確計(jì)算及精度丟失問題
在工作中經(jīng)常會(huì)遇到數(shù)值精度問題,比如說使用float或者double的時(shí)候,可能會(huì)有精度丟失問題,下面這篇文章主要給大家介紹了關(guān)于Java使用BigDecimal公式精確計(jì)算及精度丟失問題的相關(guān)資料,需要的朋友可以參考下2023-01-01

