動態數據源切換核心在於基於 ThreadLocal 的上下文持有者結合 Spring 的 AbstractRoutingDataSource 抽象路由機制。運行時根據業務邏輯動態決定當前線程使用哪個數據源,實現讀寫分離、多租戶隔離等場景。
DbContextHolder)public class DbContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDbType(String dbType) { contextHolder.set(dbType); }
public static String getDbType() { return contextHolder.get(); }
public static void clearDbType() { contextHolder.remove(); }
}
設計要點:使用 ThreadLocal 保證線程隔離,每個線程獨立維護數據源標識,避免併發衝突。
DynamicDataSource)public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
繼承 Spring 抽象類,只需實現 determineCurrentLookupKey() 返回當前數據源 Key,框架自動完成路由分發。
spring.xml)<!-- 兩個業務數據源 -->
<bean id="jksh" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver.jksh}" />
<property name="url" value="${url.jksh}" />
<property name="username" value="${username.jksh}" />
<property name="password" value="${password.jksh}" />
<!-- 連接池參數 -->
</bean>
<bean id="jclt" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- 同理配置第二個數據源 -->
</bean>
<!-- 動態路由數據源 -->
<bean id="dataSource" class="com.jclt.service.commons.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="jksh" value-ref="jksh" />
<entry key="jclt" value-ref="jclt" />
</map>
</property>
<property name="defaultTargetDataSource" ref="jksh" /> <!-- 默認數據源 -->
</bean>
public class DataSourceSwitchDemo {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("client-beans.xml");
// 切換至 jclt 數據源
DbContextHolder.setDbType("jclt");
queryUser("jclt");
// 切換至 jksh 數據源
DbContextHolder.setDbType("jksh");
queryUser("jksh");
}
private static void queryUser(String dbName) {
DataSource ds = (DataSource) ctx.getBean("dataSource");
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(ds);
bean.setConfigLocation(new FileSystemResource("src/main/resources/ibatis-config.xml"));
try (SqlSession session = bean.getObject().openSession()) {
User user = session.selectOne("com.jclt.service.Dao.readJKSH.findOne");
System.out.println("[" + dbName + "] " + user.getName());
} catch (Exception e) { e.printStackTrace(); }
}
}
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(targetDataSource)")
public void switchDataSource(TargetDataSource targetDataSource) {
DbContextHolder.setDbType(targetDataSource.value());
}
@After("@annotation(targetDataSource)")
public void clearDataSource(TargetDataSource targetDataSource) {
DbContextHolder.clearDbType(); // 清理防止污染下一請求
}
}
// 使用註解標記
@TargetDataSource("jclt")
public void queryJcltData() { ... }
@Transactional 與數據源切換切面順序:先切換數據源,再開啟事務| 問題 | 解決方案 |
|---|---|
| 線程池複用導致數據源洩漏 | 任務執行完畢必須調用 clearDbType(),或使用 DelegatingRunnable 包裝任務自動清理 |
| 只讀從庫路由 | 擴展註解支援 @ReadOnly 自動路由至從庫 |
| 啟動時默認數據源未就緒 | 配置 lazy-init="true" 或實現 InitializingBean 檢查連接 |
基於 AbstractRoutingDataSource + ThreadLocal 的動態數據源方案輕量級、無侵入性,適合多數中小型多數據源場景。生產環境建議結合 AOP 註解驅動,並嚴格管理線程上下文生命周期,避免數據源洩漏導致的數據混亂。