第一章 需求背景與技術選型
1.1 多數據源場景概述
在大型企業級應用中,單一數據庫往往無法滿足高并發和多業務線的需求,因此需要引入 多數據源 的架構設計。常見的多數據源場景包括:讀寫分離、多租戶、分庫分表以及數據源負載均衡等。
讀寫分離:在此場景下,通常有一個主庫(Master)用于寫操作,以及一個或多個從庫(Slave)用于讀操作。應用層可以將寫請求路由到主庫,讀請求路由到從庫,以減輕主庫負載并提高讀寫吞吐量。例如,在電子商務系統中,用戶下單會寫入主庫,而商品瀏覽、列表查詢等場景可以走從庫,從而提高整體性能。
多租戶:在 SaaS 系統中,不同租戶的數據需要隔離。實現方式有獨立庫、多庫共享架構等。通常根據當前租戶ID動態選擇相應的數據源,以達到數據隔離的目的。比如一個 CRM 系統,不同公司(租戶)使用相同代碼,但數據存儲在不同的數據庫中。
分庫分表(Sharding):為了處理海量數據,會將數據水平切分到多個庫或表中。按照業務切分規則(如用戶ID、時間等)路由到對應的數據源。這樣可以并行擴展數據庫容量和查詢吞吐率。
負載均衡:在微服務或高可用場景下,可能部署多個相同功能的數據庫實例。可以通過策略(輪詢、權重等)將請求均勻分布到不同數據源,提高可用性和并發處理能力。
此外,在某些復雜場景中,還可能需要支持多種數據庫類型(關系型、NoSQL 混合)或動態增加新數據源的能力。抽象的數據源路由策略能夠統一管理數據源選取邏輯,而不侵入業務代碼,這就是 AbstractRoutingDataSource
等技術產生的背景。
1.2 MyBatis 與 Spring Boot 生態的集成優勢
隨著 Java 微服務和云原生架構的普及,Spring Boot+MyBatis 已成為常見的開發棧。Spring Boot 提供自動化配置和豐富生態,如 spring-jdbc
、spring-tx
等;MyBatis 提供簡潔靈活的 ORM 框架,將 SQL 與 Java 對象映射無縫結合。兩者集成有以下優勢:
易用的配置:Spring Boot 自動化裝配機制可以簡化數據源、事務管理和 MyBatis 的配置。只需引入
spring-boot-starter-jdbc
、mybatis-spring-boot-starter
等依賴并在application.yml
中配置數據源即可使用。靈活的插件機制:MyBatis 支持插件(Interceptor)機制,可以在執行 SQL 之前或之后插入自定義邏輯。這為動態切換數據源提供了另一種實現思路(如自定義 MyBatis 攔截器來切換 DataSource)。
豐富的事務支持:Spring 框架提供統一的事務管理抽象,包括聲明式事務注解和編程事務 API,可與動態數據源無縫集成;例如 Spring DataSource 事務管理可以自動適配切換后的目標數據源。
生態插件和庫:除了手工實現,還有成熟的開源項目支持動態數據源管理,如 MyBatis-Plus 動態數據源插件、Apache ShardingSphere 等,可用于更高級的數據路由和分片需求。
通過 Spring Boot 和 MyBatis 的集成,我們可以在保持業務代碼簡潔的同時,利用框架特性來做數據源路由和切換,而不需要在每個 DAO 調用中手工傳遞 DataSource 信息。
1.3 動態數據源切換的必要性
在多數據源場景下,硬編碼數據源會導致配置僵化和代碼侵入。動態數據源切換提供一種透明、非侵入式的方式:業務代碼僅需關注要執行的業務操作,由底層框架根據當前上下文動態選擇合適的數據源。這樣做的必要性包括:
解耦業務與數據源邏輯:業務層無需感知當前要使用哪個庫,調用 DAO 接口即可。通過切換邏輯(如通過注解、上下文參數等)將路由策略集中管理,提高代碼可維護性。
靈活應對需求變化:運行時可以根據配置或上下文動態添加新數據源、讀寫比例調整、租戶增減等。無需停服重新部署即可擴展數據源體系。
滿足性能與隔離需求:對于讀寫分離場景,通過切換到從庫可以緩解主庫壓力;對于多租戶場景,根據租戶上下文路由到對應數據庫,保證數據隔離。
減少重復實現:不需要在每個 Service/DAO 中手寫切換邏輯。使用統一的
AbstractRoutingDataSource
或 AOP 切面等方案,可以復用切換代碼。
總之,動態數據源切換是為了解決多數據庫環境下靈活路由和管理數據源的需求。下文將深入分析底層實現原理,并以 Spring Boot 為例給出生產級的實踐方案。
第二章 核心組件與實現原理
2.1 AbstractRoutingDataSource 源碼解析
Spring Framework 提供了一個核心組件 AbstractRoutingDataSource
用于數據源路由。根據官方文檔的描述,它是“一個抽象的 DataSource 實現,用于基于查找鍵(lookup key)將 getConnection()
調用路由到多個目標 DataSource 之一”。核心實現邏輯如下:
路由數據源配置:
AbstractRoutingDataSource
擁有一個targetDataSources
映射,其中鍵是查找鍵(Object,一般為 String、Enum 等),值是實際的 DataSource 對象。還可以配置一個默認數據源defaultTargetDataSource
。查找鍵獲取:
AbstractRoutingDataSource
定義了一個抽象方法determineCurrentLookupKey()
,子類需重寫此方法來決定當前調用應使用哪個鍵。通常,這個鍵信息會從某種線程上下文中獲取(如 ThreadLocal 存儲的租戶ID或讀寫標識)。DataSource 路由:在每次調用
getConnection()
時,AbstractRoutingDataSource
會首先調用determineCurrentLookupKey()
獲取當前鍵,然后根據這個鍵在targetDataSources
中查找對應的實際 DataSource。如果找不到匹配的數據源,則會根據lenientFallback
屬性決定是拋出異常還是使用默認數據源。初始化與刷新:
AbstractRoutingDataSource
實現了InitializingBean
接口,其中的afterPropertiesSet()
方法會解析配置,將指定的數據源對象解析為實際的DataSource
實例,并存入內部結構供路由使用。
簡而言之,AbstractRoutingDataSource
是一個“路由器”,它本身也實現了 DataSource
接口,但內部并不存儲連接,而是根據當前上下文查找真正的目標 DataSource,然后將 getConnection()
的調用委派給該目標數據源。這就像一個中間層:業務代碼通過此代理 DataSource 調用數據庫,代理會根據規則動態決定鏈接到哪個實際數據庫。這個原理類似于春季博客所說的:“路由DataSource作為中介,真正的 DataSource 可以在運行時動態確定”。
AbstractRoutingDataSource 工作流程
應用啟動時,Spring 容器會實例化
AbstractRoutingDataSource
的子類,調用其afterPropertiesSet()
方法解析配置文件中定義的所有目標數據源并存入一個 Map。業務層注入的是這個
AbstractRoutingDataSource
代理對象,而非單一的 DataSource。在執行數據庫操作時(例如通過 MyBatis 打開 SqlSession 或 Spring 的
JdbcTemplate
調用),會觸發AbstractRoutingDataSource.getConnection()
。該方法調用
determineCurrentLookupKey()
獲取當前數據源的鍵。例如,在多租戶場景下,determineCurrentLookupKey()
可能返回當前線程所對應的租戶ID。根據查找鍵從預先配置的 Map 中取得對應的實際 DataSource,并調用其
getConnection()
獲得 JDBC 連接。如果查找鍵沒有對應的數據源,則根據
lenientFallback
決定:若允許回退,則使用默認數據源,否則拋出異常。
可以說,AbstractRoutingDataSource
將數據源選取邏輯與應用業務邏輯解耦。用戶只需關注如何獲取當前上下文的鍵,而實際的數據庫連接細節由框架完成。
2.2 ThreadLocal 上下文管理機制
在動態數據源切換中,通常需要一種機制來保存當前線程所需使用的數據源標識。Java 提供了 ThreadLocal
類,用于在線程之間隔離存儲變量:每個線程都會持有一份 ThreadLocal
變量的副本。典型的做法是使用 ThreadLocal
來存放當前線程要使用的數據源名稱或Key。
以常見的設計模式為例:定義一個 DataSourceContextHolder
(或稱 DynamicDataSourceContextHolder
)類,其內部維護一個 ThreadLocal
變量(有的實現用 Deque
來支持嵌套調用)。調用方法通常是:在進入需要切換數據源的邏輯前,先將目標數據源Key壓入 ThreadLocal
;業務代碼執行時,AbstractRoutingDataSource
會調用 determineCurrentLookupKey()
方法,此方法從 ThreadLocal
中取出當前Key。執行完畢后,再清除或彈出 ThreadLocal
中的Key,避免污染后續請求。
例如,在開源項目中,數據源上下文管理器可能是這樣的結構:
public final class DynamicDataSourceContextHolder {private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = ThreadLocal.withInitial(ArrayDeque::new);public static String peek() {Deque<String> deque = CONTEXT_HOLDER.get();return deque.peek();}public static void push(String ds) {CONTEXT_HOLDER.get().push(ds);}public static void poll() {Deque<String> deque = CONTEXT_HOLDER.get();deque.poll();}
}
上例使用 ThreadLocal<Deque<String>>
來維護一個堆棧,支持嵌套切換,這在多層服務調用需要多次切換時很有用。每個線程都會有自己的 Deque
,因此數據源的設置互不干擾。多個實現示例表明,核心思路都是“通過 ThreadLocal
管理數據源標識,然后在執行SQL前獲取當前標識并完成切換”。
需要注意的是,使用 ThreadLocal
時應謹慎清理。Web 應用中線程復用(如線程池)會導致如果不在請求結束時清空 ThreadLocal
,下一個請求可能錯誤地繼承了上一個請求的標識(即所謂的ThreadLocal 污染問題,本章后續會討論)。因此,常在切面或攔截器的 finally
代碼塊中彈出/清除 ThreadLocal
數據,以保證線程干凈。
2.3 AOP 攔截器與 MyBatis 攔截器的對比
實現動態數據源切換時,常用的方法有基于 Spring AOP 切面注解以及基于 MyBatis 插件 的攔截。兩者的原理和使用場景有所不同:
Spring AOP 切面:通過自定義注解(如
@DataSource
)標記在業務方法或類上,然后編寫一個 AOP 切面(Aspect),在切面中通過@Around
通知來在方法執行前設置線程上下文中的數據源,然后執行方法,最后在finally
中清除上下文。Spring AOP 本質上使用動態代理(JDK 或 CGLIB)來攔截對 Spring 管理 Bean 的方法調用,適用于需要在 Service 層方法調用時切換數據源的場景。優點是使用簡單、可讀性好;缺點是僅對由 Spring 容器管理的 Bean 生效,對內部調用(self-invocation)或非 Spring 托管對象不可用。MyBatis 插件攔截:MyBatis 提供了插件機制,可以攔截 Executor、StatementHandler、ResultSetHandler、ParameterHandler 等對象的執行方法。可以編寫一個 MyBatis
Interceptor
(實現org.apache.ibatis.plugin.Interceptor
并用@Intercepts
注解指定攔截點),在intercept()
方法中根據當前上下文選擇數據源。這樣做的優點是作用于 MyBatis 層,可以攔截所有通過 MyBatis 執行的 SQL 請求,不依賴 Spring AOP;缺點是實現較復雜,且 MyBatis 插件攔截點一般在執行 SQL 語句時才起作用,無法像 AOP 那樣輕松在方法入口處處理業務邏輯。動態代理模式:動態數據源本身就是一種代理模式——
AbstractRoutingDataSource
就是對目標數據源的代理。另外,Spring AOP 使用的正是動態代理機制,對目標 Bean 生成代理對象,對外提供切面功能。從性能角度看,代理調用會帶來微小開銷,但通常可以忽略。正如社區討論所說,“使用基于代理的 AOP,每應用一個切面只會多一次方法調用,性能開銷幾乎可以忽略”。MyBatis 插件也是通過包裝底層對象實現攔截,性能差異很小。除非在極端性能要求的場景下,一般不需要擔心 AOP vs 插件 vs 代理的性能差異(每次切換僅相當于額外幾次方法調用或反射調用,耗時在納秒級)。
下面舉個對比總結:
切面(AOP):基于業務邏輯層,靈活使用注解進行數據源切換,代碼可讀性好,易于集成事務。但僅攔截 Spring 管理的 Bean 方法,不適用于 Mapper 接口的內部調用。
MyBatis 插件:直接作用于 SQL 執行層,可攔截所有 MyBatis 調用,適合在 Mapper 級別強制切換數據源(比如讀操作全部攔截到從庫)。配置繁瑣度較高,一般配合 ThreadLocal 使用。
動態代理:本質機制,與 AOP 類似。Spring AOP 在 bean 層使用 JDK/CGlib 動態代理技術;MyBatis 插件在 Executor 層使用代理。總體上,都帶來很小的調用開銷。
在實際應用中,最常用的方案是自定義注解 + Spring AOP 切面 的組合。這種方案直觀且結合 Spring Boot 注解驅動編程體驗好。另一個常見做法是使用成熟的動態數據源框架(如 MyBatis-Plus 提供的 @DS
注解),其內部也利用 AOP + ThreadLocal 實現。當然,根據業務需要,也可以在 MyBatis 插件層面實現切換,但需要開發者深入理解 MyBatis 攔截原理。
第三章 Spring Boot 實現方案
本章我們將以 Spring Boot 為例,從零開始演示如何實現動態數據源切換,包括配置數據源、定義注解和切面、編寫測試等。
3.1 多數據源配置類編寫
首先,需要在 Spring Boot 項目中配置多個數據庫連接。在 application.yml
中定義多個數據源,如 master 和 slave:
spring:datasource:master:url: jdbc:mysql://localhost:3306/master_dbusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driverslave:url: jdbc:mysql://localhost:3306/slave_dbusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driver
然后,在 Java 代碼中定義兩個 DataSource
Bean。例如:
@Configuration
public class DataSourceConfig {@Bean(name = "masterDataSource")@ConfigurationProperties(prefix = "spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean(name = "slaveDataSource")@ConfigurationProperties(prefix = "spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}
}
這里使用 @ConfigurationProperties
從 YAML 注入屬性,DataSourceBuilder
可以自動選擇 HikariCP 或 Druid 等連接池。接下來需要定義一個 動態路由數據源,繼承 AbstractRoutingDataSource
并實現 determineCurrentLookupKey()
:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.peek();}
}
同時,編寫配置將上述兩個數據源注入到動態路由數據源中:
@Configuration
public class DynamicDataSourceConfig {@Autowired@Qualifier("masterDataSource")private DataSource masterDataSource;@Autowired@Qualifier("slaveDataSource")private DataSource slaveDataSource;@Beanpublic DynamicRoutingDataSource dynamicDataSource() {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("master", masterDataSource);targetDataSources.put("slave", slaveDataSource);DynamicRoutingDataSource ds = new DynamicRoutingDataSource();ds.setTargetDataSources(targetDataSources);ds.setDefaultTargetDataSource(masterDataSource); // 設置默認數據源return ds;}// 配置 SqlSessionFactory,使 MyBatis 使用動態數據源@Beanpublic SqlSessionFactory sqlSessionFactory(DynamicRoutingDataSource dynamicDataSource) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dynamicDataSource);// 可設置 MyBatis 配置、映射文件等return factoryBean.getObject();}
}
在上述配置中,我們創建了兩個命名的 DataSource Bean,再用 DynamicRoutingDataSource
將它們放到一個 Map 中,key 為數據源標識(如 "master"、"slave"),并設置默認數據源。DynamicRoutingDataSource
繼承 AbstractRoutingDataSource
,會在運行時根據 determineCurrentLookupKey()
返回的鍵決定使用哪個子數據源。我們再把 DynamicRoutingDataSource
作為 MyBatis 的 SqlSessionFactory 的數據源,確保所有 MyBatis 操作都經過它。
Spring Boot 項目結構示例:
src/main/java/com/example/dynamicds/config/DataSourceConfig.java // 配置 master/slave DataSourceDynamicDataSourceConfig.java // 配置 DynamicRoutingDataSource、SqlSessionFactoryannotation/DataSource.java // 自定義注解aspect/DataSourceAspect.java // AOP 切面holder/DynamicDataSourceContextHolder.java // ThreadLocal 上下文mapper/UserMapper.java // MyBatis Mapper 接口entity/User.java // 實體類service/UserService.java // 業務層接口及實現DynamicDataSourceApplication.java // 啟動類
src/main/resources/application.yml // 數據源配置mapper/UserMapper.xml // MyBatis 映射文件
pom.xml // 依賴配置
pom.xml(部分示例依賴):
<dependencies><!-- Spring Boot 及其 Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- MyBatis Spring Boot Starter --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><!-- 數據庫連接池(可選 HikariCP)--><dependency><groupId>com.zaxxer</groupId><artifactId>HikariCP</artifactId></dependency><!-- MySQL 驅動 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- Spring AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
</dependencies>
上面代碼中核心在于將多個 DataSource
注入到 DynamicRoutingDataSource
中,通過 setTargetDataSources
指定路由映射。此外,我們使用 @Bean
注冊了 SqlSessionFactory
,將 DataSource
設置為我們的動態數據源。這樣,后續所有的數據庫操作都會走 DynamicRoutingDataSource
,由其決定實際連接哪個庫。
3.2 自定義 @DataSource
注解與 AOP 切面實現
為了在業務層靈活選擇數據源,我們可以定義一個自定義注解(如 @DataSource
),并通過 Spring AOP 切面來攔截帶該注解的方法。在切面中,我們將注解中的數據源名稱壓入 DynamicDataSourceContextHolder
的 ThreadLocal
。
自定義注解 DataSource.java
:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {String value() default "master";
}
該注解可標注在類或方法上,value
用于指定使用的數據源名稱。
上下文持有器 DynamicDataSourceContextHolder.java
:
public class DynamicDataSourceContextHolder {private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = ThreadLocal.withInitial(ArrayDeque::new);// 獲得當前線程的數據源public static String peek() {Deque<String> deque = CONTEXT_HOLDER.get();return deque.peek();}// 將數據源壓入棧頂public static void push(String ds) {CONTEXT_HOLDER.get().push(ds);}// 彈出當前數據源public static void poll() {CONTEXT_HOLDER.get().poll();}
}
如前所述,這個 ThreadLocal
棧結構允許嵌套調用時恢復前一個數據源。
AOP 切面 DataSourceAspect.java
:
@Aspect
@Component
public class DataSourceAspect {@Pointcut("@annotation(com.example.dynamicds.annotation.DataSource) || @within(com.example.dynamicds.annotation.DataSource)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {// 解析注解上的數據源名稱MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();String ds = "master"; // 默認數據源if (method.isAnnotationPresent(DataSource.class)) {DataSource annotation = method.getAnnotation(DataSource.class);ds = annotation.value();} else {// 如果方法上沒有,查看類上是否有注解Class<?> targetClass = point.getTarget().getClass();if (targetClass.isAnnotationPresent(DataSource.class)) {DataSource annotation = targetClass.getAnnotation(DataSource.class);ds = annotation.value();}}try {// 切換數據源DynamicDataSourceContextHolder.push(ds);return point.proceed();} finally {// 切換完畢后,彈出數據源DynamicDataSourceContextHolder.poll();}}
}
上面代碼說明:切面攔截標注了 @DataSource
注解的方法(或類),優先取方法上的注解值,否則取類上的。獲取到的 ds
就是要使用的數據庫標識(如 "slave")。在執行業務方法前,將該標識 push
到 ThreadLocal
棧;方法執行完成后,再彈出,確保后續線程不受影響。
這樣就形成了完整的 注解驅動 + AOP 切面 的動態切換方案:當調用一個被 @DataSource("slave")
標記的方法時,切面會把 ThreadLocal
里當前數據源設置為 "slave",從而使 determineCurrentLookupKey()
返回 "slave",動態數據源路由到從庫。
示例業務代碼:
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 默認使用 masterpublic List<User> listUsers() {return userMapper.selectAll();}@DataSource("slave")public List<User> listUsersFromSlave() {return userMapper.selectAll();}
}
在這個例子中,listUsersFromSlave()
方法被標記為使用 "slave" 數據源,調用時會路由到從庫執行查詢。核心邏輯完全由切面和路由組件完成,業務層無需關心數據源切換的細節。
3.3 動態數據源切換的測試用例設計
為了確保動態數據源切換正確,可以編寫單元測試或集成測試驗證。思路是:在測試中通過設置上下文(類似切面方式)切換數據源,執行 CRUD 操作,并檢查結果是否來自預期的庫。
示例測試:
@SpringBootTest
public class DynamicDataSourceTest {@Autowiredprivate UserMapper userMapper;@Testpublic void testMasterSlaveSwitch() {// 準備:在 master 和 slave 中插入可區分的數據// 假設在 masterDB 中有 user {id=1, name="A"},slaveDB中有 user{id=2, name="B"}// 默認使用 masterList<User> masterUsers = userMapper.selectAll();assertTrue(masterUsers.stream().anyMatch(u -> u.getName().equals("A")));// 切換到 slaveDynamicDataSourceContextHolder.push("slave");try {List<User> slaveUsers = userMapper.selectAll();assertTrue(slaveUsers.stream().anyMatch(u -> u.getName().equals("B")));} finally {DynamicDataSourceContextHolder.poll();}}
}
上例使用了手動 push
/poll
的方式模擬切換。在更高層的測試框架下,可以直接調用帶注解的方法來進行測試。關鍵在于驗證:在切換前后得到的結果應該明顯不同,以證明數據是從不同的數據源中讀取。
另一個常見測試是多線程環境的切換:確保在并發執行切換時,各線程能夠正確分辨使用不同的數據源且互不干擾。可以使用多線程測試框架或模擬請求的方式,驗證線程安全性。
測試注意點:
在測試數據庫中插入明確可區分的數據(不同庫中插入不同內容)進行驗證。
在單元測試結束后清理
ThreadLocal
,或者在測試類中加上@After
注解清理上下文。如果使用 Spring 事務,需要特別注意默認事務配置下切面是否按預期執行(可能需要設置事務為
REQUIRES_NEW
或測試中手動處理事務)。
通過上述配置和測試,基礎的動態數據源切換功能就能正確運行。在此基礎上,下一章我們將探討更高級的功能和優化。
第四章 進階實踐與優化
4.1 數據源自動注冊與動態加載
在生產環境中,有時需要運行時動態新增或移除數據源。例如多租戶平臺中租戶隨時注冊或注銷,或者需要在不重啟服務的情況下將新數據庫接入系統。實現思路是利用 AbstractRoutingDataSource
的內部結構:在運行時修改其內部的目標數據源映射(targetDataSources
),并調用 afterPropertiesSet()
刷新路由。
以前述 DynamicRoutingDataSource
為例,我們可以擴展接口來動態添加:
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {// 存放所有數據源實例private Map<Object, DataSource> dataSourceMap = new ConcurrentHashMap<>();public void addDataSource(String dsKey, DataSource dataSource) {dataSourceMap.put(dsKey, dataSource);// 添加到 AbstractRoutingDataSource 的 targetDataSourcessuper.setTargetDataSources(dataSourceMap);super.afterPropertiesSet(); // 刷新解析}@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceContextHolder.peek();}
}
在上述例子中,當需要新增數據源時,可以通過 dynamicRoutingDataSource.addDataSource("newDs", newDataSource)
來實現。注意調用 afterPropertiesSet()
以讓 AbstractRoutingDataSource
重新解析新的配置。同樣地,可以實現 removeDataSource
方法從 Map 中刪除并刷新。
這種動態注冊方式允許我們在運行時基于配置中心、后臺管理等方式來管理數據源。例如,可以在服務中添加一個管理接口:
@RestController
public class DataSourceController {@Autowiredprivate DynamicRoutingDataSource dynamicDataSource;@PostMapping("/datasource")public String addDataSource(@RequestBody DataSourceProperties props) {DataSource ds = DataSourceBuilder.create().driverClassName(props.getDriver()).url(props.getUrl()).username(props.getUsername()).password(props.getPassword()).build();dynamicDataSource.addDataSource(props.getName(), ds);return "DataSource added";}
}
以上代碼接收一個 POST 請求,傳遞新的數據庫配置,動態創建并注冊數據源。需要注意線程安全和異常處理(例如若添加重復鍵應拋出錯誤)。動態加載最大的挑戰是:確保在切換發生過程中,所有組件(事務管理、SqlSession 等)都能感知到更新過的路由表,避免并發時出現找不到數據源的情況。
4.2 數據源切換異常的兜底策略
在多數據源路由中,常見的問題是找不到目標數據源,或者沒有配置默認數據源時的處理。Spring 的 AbstractRoutingDataSource
提供了 寬松回退(lenientFallback) 機制。其含義是:當 determineCurrentLookupKey()
返回的鍵在 targetDataSources
中沒有匹配時,如果開啟寬松回退,則會自動使用默認數據源;否則會拋出 IllegalStateException
。
默認情況下,lenientFallback
為 true,即非嚴格模式,只有當查找鍵對應的 DataSource 為空才回落到默認數據源。可通過配置類或 XML 調用 setLenientFallback(false)
,在查找鍵未配置時拋出異常,以便快速定位問題。無論如何,推薦在系統啟動時至少提供一個默認數據源,可以防止在切換出錯時導致所有操作都失敗。
如果決定關閉回退(lenientFallback=false
),需要注意:
如果切換的
ThreadLocal
鍵發生意外錯誤(如未設置或清理不當導致為空),則會拋出異常,因此生產代碼中應保證在使用前必須設置鍵,或在注解切面中做默認保護。對于不使用
@DataSource
注解的方法(未明確切換),我們一般讓其走默認數據源。可以在上下文取值時做判斷,如String ds = DynamicDataSourceContextHolder.peek(); if (ds == null) ds="master";
。
另外,當數據源不存在時,還可以自定義兜底邏輯。例如,捕獲拋出的 DataAccessException
或 SQLException
并記錄錯誤信息或發送告警。而對于讀寫分離場景,可以制定“失敗重試”策略:若切換到從庫后讀操作失敗,再自動重試主庫。
總之,需要合理配置默認源和回退策略,并在代碼層面做好異常處理,避免生產時因為數據源名錯誤而全局不可用。Spring 參考答案也曾提到,如果完全不想出現默認數據源,可考慮禁用 Hibernate 的自動 DDL 和元數據校驗,因為初始化階段可能會觸發數據庫連接,否則可以通過配置 spring.jpa.hibernate.ddl-auto=none
、spring.jpa.properties.javax.persistence.validation.mode=none
等避免啟動時的連接嘗試。
4.3 性能調優技巧(連接池參數配置)
動態數據源切換本身并不會顯著影響性能,但多數據源環境下需要特別關注每個數據源連接池參數的配置。合理配置可提升并發能力和穩定性。以下是一些常見的優化建議:
連接池類型與并發能力:Spring Boot 默認使用 HikariCP 作為連接池(若引入
spring-boot-starter-jdbc
)。HikariCP 以高性能著稱,可通過調整spring.datasource.hikari.maximum-pool-size
來控制最大連接數。默認值為 10;在高并發場景下可以根據服務器資源適當增大,比如 50、100。但要避免配置過大導致數據庫壓力。連接超時與空閑超時:
connectionTimeout
(獲取連接等待時間)默認 30 秒。可根據業務需求調整,防止線程長時間阻塞等待連接。idleTimeout
(連接空閑超時)和maxLifetime
(連接最大存活時間)也應合理設置,避免頻繁創建銷毀連接;推薦一個較長的maxLifetime
(如 30 分鐘或更高),防止頻繁重建。多環境配置分離:開發、測試環境可以使用較小的連接池規模;生產環境可根據實際負載增加連接數。同時生產中若讀寫分離,應為從庫和主庫分別配置連接池(例如主庫承載寫請求,可配置更大連接數;從庫承載讀請求,可根據讀請求量配置)。
監控連接池狀態:集成監控工具(如 HikariCP 自帶的指標、或 Micrometer+Prometheus)來實時觀察各個數據源的連接使用情況,及時調整參數。
SQL 調優與批量操作:對于動態路由邏輯應盡量減少切換開銷,避免在批量操作中頻繁切換數據源。對于大量寫操作,推薦批量提交或使用批處理方式。
緩存與命中率:如果使用 Redis 等緩存減輕數據庫壓力,同樣需要根據多數據源環境配置緩存鍵策略,避免不同庫間數據污染。
最后,可以引用工具鏈的診斷數據來驗證優化效果。例如使用 Spring Boot 提供的 Actuator 監控數據源狀態,或通過 select * from information_schema.processlist
查看連接情況。正確的連接池參數可以顯著減少連接建立/關閉的開銷,提高持續負載下的穩定性。
第五章 生產環境注意事項
5.1 線程安全與 ThreadLocal 污染問題
如前文所述,動態數據源切換依賴 ThreadLocal
來保存當前線程的數據源標識。生產環境通常使用線程池(如 Tomcat 池、WebFlux 線程池等)來復用線程,此時ThreadLocal 污染風險需要特別關注:如果在線程執行完成后未及時清理 ThreadLocal
,下一個任務復用該線程時可能誤用上一個任務的數據源標識。
常見防范方法:
切面 finally 清理:在 AOP 切面中,務必使用
try...finally
結構,在finally
塊中調用DynamicDataSourceContextHolder.poll()
(彈出當前數據源)或remove()
來清空標識。確保無論業務邏輯是否異常結束,都能清理上下文。檢查默認值:在上下文管理類中,如
DynamicDataSourceContextHolder.peek()
返回null
或空字符串時,應該默認使用主庫。這可以作為保險機制,避免因清理不當導致 lookupKey 為空時拋出異常。避免同一線程并行處理多任務:在特殊場景下,比如使用 CompletableFuture、ForkJoinPool 等框架時,一個線程可能并行處理多個任務或等待任務結果,最好不要在異步回調中依賴 ThreadLocal。需要時可使用
Executor
代理或ThreadLocal
繼承(InheritableThreadLocal)來傳遞上下文,但要格外謹慎。定期審查日志:當出現請求訪問錯誤數據庫或返回數據錯亂時,懷疑是線程污染時,應在日志中記錄切換和清理操作,比如在切面中加入日志:
try {DynamicDataSourceContextHolder.push(ds);logger.debug("Switch DataSource to [{}]", ds);return point.proceed(); } finally {DynamicDataSourceContextHolder.poll();logger.debug("Revert DataSource"); }
這樣在異常情況可查看是否某條請求沒有執行清理而影響了后續請求。
總之,“線程安全”是動態數據源方案必須關注的。ThreadLocal 本身是線程隔離的,只要使用正確的編程模式(及時清理、避免跨線程傳播),在多線程環境下仍可安全使用動態切換。反之,若忽視清理,就會出現“線程池復用導致數據源標識錯亂”的問題。我們可以將這種情況比喻為“快遞分揀系統”:每輛車(線程)在發出新的路線指令(數據源)時,必須清除之前的路線,否則貨物(數據)會送錯地方。
5.2 數據源切換與事務管理的兼容性
在 Spring 中使用動態數據源切換時,事務管理也是一個重點難點。常見問題包括:事務邊界應在數據源切換之后設定,以及跨數據源事務的兼容。
事務與切換順序:通常我們希望切換數據源后再開啟事務。因為事務管理器會綁定到具體數據源連接上。如果先開啟事務(Spring 的
@Transactional
注解默認在切面之后執行),再切換數據源,則事務仍然作用在舊數據源上。解決方法有兩種:切換邏輯早于事務:可以使用
@Transactional
注解的@EnableTransactionManagement(order = X)
屬性調整切面執行順序,確保數據源切換的 AOP 執行順序高于事務 AOP。通常給切換切面較高優先級(較低 order 值)。聲明式事務使用動態事務管理器:配置
AbstractRoutingDataSource
作為 DataSource,使得 Spring 事務管理通過它獲取連接,這樣即使事務切面在切換切面之后,也會從路由數據源中獲取正確的實際連接。
多數據源事務:如果業務邏輯需要同時操作多個庫(如跨租戶查詢),則單個事務無法跨多個數據源管理。解決方案是:
使用分布式事務:如使用 Seata、Atomikos 等第三方分布式事務管理框架,協調多個數據源事務。
自行管理事務:在最簡單的場景下,可以明確不同數據源的操作由不同事務管理,并在業務層分別開啟事務,然后手動協調(這種方式較為復雜且容易出錯,不推薦)。
只讀事務與寫事務:結合讀寫分離場景,如果在只讀方法上使用
@Transactional(readOnly = true)
,可以讓其默認切換到從庫。而在寫方法上使用默認事務,可以切換到主庫。這一做法需要我們在切面邏輯中可判斷事務屬性,例如:boolean readOnly = ((MethodSignature)point.getSignature()).getMethod().getAnnotation(Transactional.class).readOnly(); if (readOnly) {DynamicDataSourceContextHolder.push("slave"); } else {DynamicDataSourceContextHolder.push("master"); }
結合
@Transactional
的readOnly
標志來自動切換,是一種常見的優化策略。需要確保事務配置優先于切換切面的次序,或者在切面中根據事務信息設置。事務回滾注意:當發生異常回滾時,由于事務可能會重新獲取連接或釋放連接,
ThreadLocal
不要在事務回滾時才進行清理(事務異常可提前觸發切面 finally 塊清理)。同時,在使用分布式事務時,需要根據所用框架的規范進行切換管理。
綜上,動態數據源切換與事務管理需要配合使用。最佳實踐是:動態切換邏輯在 Spring AOP 配置為比事務切面更高優先級,確保在獲取連接前已切換到正確的 DataSource;并在切面清理后讓事務正常關閉連接。這樣可以保證事務邏輯應用到預期的數據庫上。
5.3 日志記錄與監控埋點
為了運維和排查問題,建議在動態數據源切換的關鍵位置加入日志和監控埋點:
數據源切換日志:在 AOP 切面中增加日志輸出,如前述示例所示,當每次切換發生時記錄目標數據源。日志內容可包含類名、方法名和數據源標識,方便定位哪個服務調用產生切換。
連接獲取跟蹤:在使用
AbstractRoutingDataSource
時,可以開啟 JDBC 連接池的日志或監控,監控每個數據源的連接池狀態。例如 HikariCP 支持 JMX,可以定期查看連接使用情況。SQL 監控:可集成 MyBatis 的 SQL 日志(Spring Boot 支持
logging.level.com.example.mapper=DEBUG
)或使用像 P6Spy 這樣的工具,在日志中標記當前使用的是哪一個數據源執行的 SQL。動態數據源切換框架(如dynamic-datasource-spring-boot-starter
)通常提供 SPI 接口,可以在連接獲取時輸出額外信息。監控埋點:如果項目使用了 APM(例如 SkyWalking、Pinpoint)或自定義埋點工具,可以在切面中埋點,以跟蹤跨庫調用。記錄數據源切換、事務執行、SQL 執行時長等關鍵指標。
通過完善的日志和監控,運維人員可以快速發現配置錯誤(如數據源名稱寫錯導致使用了默認庫)、性能瓶頸(例如某個庫連接池滿載)等問題。例如,“如果發現請求日志中某些方法頻繁切換到錯誤的數據源”,則說明切面邏輯或配置有問題;如果“某個數據源的連接使用率常年接近上限”,則需要擴容或優化查詢。
第六章 擴展場景與替代方案
6.1 分庫分表場景下的數據源路由
在分庫分表場景下,通常會將數據按某種規則拆分到多個數據庫或表中。例如將用戶數據按地區或用戶ID范圍分到不同庫。動態數據源切換可作為分庫路由的基礎,但一般僅能處理庫級別的切換。如果同時需要分表,則需進一步結合 MyBatis-Plus、Sharding-Sphere 等中間件。
實現思路:首先確定分庫規則,如通過計算 (userId % N)
確定目標數據庫,然后在業務或 MyBatis 層將 ThreadLocal
切換到對應數據庫。再配合分表插件(如 MyBatis-Plus 動態表名插件或 ShardingSphere 的表策略)實現表級路由。
例如,手動實現分庫:
// 計算應當使用哪個庫
String dbKey = (userId % 2 == 0) ? "db0" : "db1";
DynamicDataSourceContextHolder.push(dbKey);
try {userMapper.insert(user);
} finally {DynamicDataSourceContextHolder.poll();
}
如果僅靠 AOP 注解不方便,也可使用 MyBatis-Plus 的分庫分表插件,它支持根據租戶ID或表鍵自動路由。MyBatis-Plus 的 MultiDataSource
插件也支持多租戶場景,兼顧分庫分表配置。
6.2 動態數據源與 MyBatis-Plus 的集成
MyBatis-Plus 提供了開源的 dynamic-datasource-spring-boot-starter
(以下簡稱 DynamicDataSource),在 Spring Boot 項目中非常流行。這個組件內置了我們上面自定義的功能,包括注解、切面、數據源注冊等。它特點如下:
注解使用:提供
@DS
注解(類似我們上面定義的@DataSource
),功能相近。自動配置:掃描
spring.datasource.dynamic.*
下的配置,自動注入所有子數據源;支持分組數據源(配置多個從庫別名為組名)。增強功能:除了動態切換,還提供如 數據源加密(ENC())、動態刷新(熱更新數據源)、獨立初始化表結構 等特性。
與 MyBatis-Plus 兼容:直接支持 MyBatis-Plus,無需額外配置;也可以和 Quartz 等庫兼容。
多租戶支持:可以自定義租戶ID獲取器,實現租戶自動注入邏輯。
集成方法大致如下:
引入依賴:根據 Spring Boot 版本選擇
com.baomidou:dynamic-datasource-spring-boot-starter
或其 Boot 3 版本。在
application.yml
中添加多數據源配置,結構同前述,只是鍵名可能略有差異(以下劃線分組)。使用注解
@DS("slave1")
切換數據源。
MyBatis-Plus 方案對開發者透明度很高,如果只是需要常規的讀寫分離和多數據源切換功能,直接使用它會比手寫更快捷。它的源碼同樣采用了 AbstractRoutingDataSource
和 ThreadLocal,并做了豐富的邊緣處理,建議閱讀其官方文檔了解更多。
6.3 其他框架方案對比(例如 ShardingSphere)
除了上述手寫和 MyBatis-Plus 插件方案,還有一些第三方中間件支持數據源動態路由:
Apache ShardingSphere:ShardingSphere-JDBC(原 Sharding-JDBC)是一個開源分布式數據庫中間件。它本身能作為一個提供分片能力的 JDBC 驅動,支持水平分庫分表、讀寫分離、分布式事務等。與 Spring Boot 集成時,將數據源 URL 改為
jdbc:shardingsphere:...
即可使用。ShardingSphere 支持配置多數據源,讀寫分離策略,以及復雜的分表規則(如提示鍵、標準分片算法等)。不過,使用 ShardingSphere 需要額外的 YAML 或 API 配置,學習成本和運維成本相對較高。它更適用于大規模分庫分表的場景。對于簡單的讀寫分離或少量數據源切換,可能用手寫切面更輕量。官方文檔說明:“可以直接將 ShardingSphereDataSource 配合 ORM 框架使用”(如 MyBatis)。Drools / 自定義路由:有些團隊也會實現自定義的數據源路由插件,或者使用 AOP+SPI 的方式,類似于 Spring 的
AbstractRoutingDataSource
變體。但這通常是極少數場景的定制方案。Spring Cloud 組件:對于微服務架構,還可以使用 Spring Cloud 提供的配置管理中心(如 Config Server)動態推送數據源配置,配合自定義上下文更新。
總的來說,各方案優缺點為:
手寫 + Spring AOP(本章介紹的方案):靈活可控,完全掌握實現細節,適合團隊有定制需求時使用。缺點是需要自行維護代碼、處理邊界情況。
MyBatis-Plus DynamicDatasource:快速上手,功能豐富,適合常規場景;依賴第三方庫升級。
ShardingSphere 等中間件:功能全面(分庫+分表+分布式事務),可視化程度高。適合對分庫分表、讀寫分離有復雜需求的項目。缺點是學習曲線陡峭,配置復雜。
在選擇上,如果項目只需要簡單的多數據源切換,且團隊熟悉 Spring 技術棧,那么手寫或 MyBatis-Plus 即可;如果項目中對分片、事務、監控要求較高,可以考慮 ShardingSphere 或類似的方案。