| 類型 | 特徵 | 典型場景 |
|---|---|---|
| 獨立資料源 | 業務無關、Schema 獨立 | 平台庫 + 遊戲庫、多租戶隔離 |
| 主從複制 | Master 寫、Slave 讀、數據最終一致 | 讀寫分離、報表查詢分流 |
適合業務完全隔離的場景,各資料源擁有獨立的 SqlSessionFactory、TransactionManager、Mapper 掃描包。
<!-- 資料源 1:平台庫 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath*:mapper/platform/**/*.xml"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.app.mapper.platform"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 資料源 2:遊戲庫 -->
<bean id="dataSourceGame" class="com.alibaba.druid.pool.DruidDataSource"> ... </bean>
<bean id="sqlSessionFactoryGame" class="org.mybatis.spring.SqlSessionFactoryBean"> ... </bean>
<bean id="transactionManagerGame" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> ... </bean>
<tx:annotation-driven transaction-manager="transactionManagerGame"/>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.app.mapper.game"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryGame"/>
</bean>
MapperScannerConfigurer 的 sqlSessionFactoryBeanName 指定對應工廠適合讀寫分離場景,單一 SqlSessionFactory,動態路由決定走 Master 或 Slave。
public enum DataSourceType { MASTER, SLAVE }
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceType> context =
ThreadLocal.withInitial(() -> DataSourceType.MASTER);
public static void set(DataSourceType type) { context.set(type); }
public static DataSourceType get() { return context.get(); }
public static void clear() { context.remove(); }
}
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.get();
}
}
<bean id="dataSourceMaster" class="com.alibaba.druid.pool.DruidDataSource"> ... </bean>
<bean id="dataSourceSlave" class="com.alibaba.druid.pool.DruidDataSource"> ... </bean>
<bean id="routingDataSource" class="com.app.config.RoutingDataSource">
<property name="defaultTargetDataSource" ref="dataSourceMaster"/>
<property name="targetDataSources">
<map key-type="com.app.enums.DataSourceType">
<entry key="MASTER" value-ref="dataSourceMaster"/>
<entry key="SLAVE" value-ref="dataSourceSlave"/>
</map>
</property>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="routingDataSource"/>
<property name="mapperLocations" value="classpath*:mapper/**/*.xml"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="routingDataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
@Aspect
@Component
public class DataSourceRoutingAspect {
// 所有以 select/find/get/query 開頭的 Service 方法走 Slave
@Pointcut("execution(* com.app.service..*.select*(..)) || " +
"execution(* com.app.service..*.find*(..)) || " +
"execution(* com.app.service..*.get*(..)) || " +
"execution(* com.app.service..*.query*(..))")
public void readMethods() {}
@Before("readMethods()")
public void routeToSlave() {
DataSourceContextHolder.set(DataSourceType.SLAVE);
}
// 寫操作、事務方法強制走 Master
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}
@Before("transactionalMethods()")
public void routeToMaster() {
DataSourceContextHolder.set(DataSourceType.MASTER);
}
// 方法結束清理
@After("readMethods() || transactionalMethods()")
public void clear() {
DataSourceContextHolder.clear();
}
}
AbstractRoutingDataSource 實現 InitializingBean,afterPropertiesSet() 將配置的 targetDataSources 轉為 resolvedDataSources MapgetConnection() → determineTargetDataSource() → determineCurrentLookupKey() 獲取 ThreadLocal 值 → Map 查找對應 DataSource| 維度 | 方案一:獨立配置 | 方案二:路由+AOP |
|---|---|---|
| 適用場景 | 業務隔離、多租戶 | 主從複制、讀寫分離 |
| 事務支援 | 單庫事務 | 單庫事務(同庫主從) |
| 跨庫事務 | 不支援 | 不支援(需 Seata/Atomikos) |
| 配置複雜度 | 低(重複配置) | 中(需 AOP 規則設計) |
| 靈活度 | 低 | 高(方法級路由控制) |
| 延遲感知 | 無 | 需業務判斷:強一致查詢走 Master |
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterRead {}
@Aspect
@Component
public class MasterReadAspect {
@Before("@annotation(MasterRead)")
public void forceMaster() {
DataSourceContextHolder.set(DataSourceType.MASTER);
}
}
// 使用
@Service
public class OrderService {
@MasterRead // 購買後立即查詢,必須見到剛寫入的數據
public Order getOrderAfterPay(Long orderId) { ... }
}
多資料源配置核心在於業務場景判斷:業務隔離選方案一,讀寫分離選方案二。方案二結合 AOP 實現「零侵入」路由,但需嚴謹設計切點表達式,並配合 @MasterRead 等註解處理強一致讀場景。生產環境建議整合 Dynamic-Datasource-Spring-Boot-Starter 等成熟框架,避免重複造輪子。