在單一應用中連接多個資料庫(如主庫寫、從庫讀、跨專案同步),需動態切換資料源。本文基於 Spring AbstractRoutingDataSource 實現註解驅動的動態切換方案,含核心原理、AOP 織入、事務協同與分散式事務探討。
AbstractRoutingDataSource 繼承 AbstractDataSource,核心邏輯在 determineTargetDataSource():
protected DataSource determineTargetDataSource() {
Object lookupKey = determineCurrentLookupKey(); // 抽象方法,由子類實現
DataSource ds = this.resolvedDataSources.get(lookupKey);
return ds != null ? ds : this.resolvedDefaultDataSource;
}
子類只需實現 determineCurrentLookupKey() 回傳當前資料源 Key。
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String key) { CONTEXT.set(key); }
public static String get() { return CONTEXT.get(); }
public static void clear() { CONTEXT.remove(); }
}
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master";
}
@Aspect
@Order(1) // 必須在事務切面之前執行
public class DataSourceSwitchAspect {
@Before("@annotation(ds)")
public void switchDataSource(DataSource ds) {
DataSourceContextHolder.set(ds.value());
}
@After("@annotation(ds)")
public void clearDataSource(DataSource ds) {
DataSourceContextHolder.clear();
}
}
<bean id="masterDs" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="${master.url}"/>
<property name="username" value="${master.user}"/>
<property name="password" value="${master.pwd}"/>
</bean>
<bean id="slaveDs" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="${slave.url}"/>
<property name="username" value="${slave.user}"/>
<property name="password" value="${slave.pwd}"/>
</bean>
<bean id="routingDs" class="com.example.datasource.DynamicRoutingDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="master" value-ref="masterDs"/>
<entry key="slave" value-ref="slaveDs"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="masterDs"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="routingDs"/>
</bean>
<aop:aspectj-autoproxy/>
@Service
public class OrderService {
@DataSource("master")
public void createOrder(Order order) { ... } // 寫主庫
@DataSource("slave")
public List<Order> queryOrders() { ... } // 讀從庫
}
資料源切換必須在 事務開啟之前 完成,否則連接已從預設資料源獲取。配置 @Order(1) 確保切換切面優先於事務切面(@Order(2))。
若單一業務需跨多資料源且要求 ACID(如:主庫寫訂單、從庫扣庫存),本地事務無法保證一致性。解決方案:
建議:儘量避免跨資料源事務,透過資料同步、異步事件最終一致性替代。
AbstractRoutingDataSource + ThreadLocal 實現線程級資料源隔離。