單一應用需連接多個業務隔離的數據庫(如:主業務庫 + 日誌分析庫、或 多租戶隔離庫)。Spring 通過多 DataSource + 多 SessionFactory + AOP 動態路由 實現透明切換。
<context:property-placeholder location="classpath:config.properties" />
# config.properties
A.driver_class=com.mysql.jdbc.Driver
A.url=jdbc:mysql://db1:3306/biz_a?useUnicode=true&characterEncoding=utf8
A.username=user_a
A.password=pass_a
B.driver_class=com.mysql.jdbc.Driver
B.url=jdbc:mysql://db2:3306/biz_b?useUnicode=true&characterEncoding=utf8
B.username=user_b
B.password=pass_b
<!-- 數據源 A -->
<bean id="dataSource_A" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${A.driver_class}" />
<property name="url" value="${A.url}" />
<property name="username" value="${A.username}" />
<property name="password" value="${A.password}" />
</bean>
<!-- 數據源 B -->
<bean id="dataSource_B" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${B.driver_class}" />
<property name="url" value="${B.url}" />
<property name="username" value="${B.username}" />
<property name="password" value="${B.password}" />
</bean>
<bean id="sessionFactory_A" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource_A" />
<property name="packagesToScan" value="com.example.modulea.entity" />
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.show_sql">false</prop>
</props>
</property>
</bean>
<bean id="sessionFactory_B" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource_B" />
<property name="packagesToScan" value="com.example.moduleb.entity" />
<!-- hibernateProperties 同上 -->
</bean>
<!-- 使用 A 庫的 DAO -->
<bean id="userDao" class="com.example.modulea.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory_A" />
</bean>
<!-- 使用 B 庫的 DAO -->
<bean id="logDao" class="com.example.moduleb.dao.LogDaoImpl">
<property name="sessionFactory" ref="sessionFactory_B" />
</bean>
避免 DAO 爆炸式增長,使用 @TargetDataSource 註解 + AOP 切面實現方法級自動切換。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
String value() default "dataSource_A"; // 默認數據源
}
public class DynamicDataSourceHolder {
private static final ThreadLocal holder = new ThreadLocal<>();
public static void put(String dsKey) { holder.set(dsKey); }
public static String get() { return holder.get(); }
public static void clear() { holder.remove(); }
}
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.get();
}
}
<bean id="dataSource" class="com.example.config.DynamicRoutingDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="dataSource_A" value-ref="dataSource_A" />
<entry key="dataSource_B" value-ref="dataSource_B" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSource_A" />
</bean>
<bean id="sessionFactory" class="...AnnotationSessionFactoryBean">
<property name="dataSource" ref="dataSource" /> <!-- 注入路由數據源 -->
</bean>
@Aspect
@Order(1) // 確保在事務開啟前切換
public class DataSourceSwitchAspect {
@Before("@annotation(targetDataSource)")
public void switchDataSource(TargetDataSource targetDataSource) {
DynamicDataSourceHolder.put(targetDataSource.value());
}
@After("@annotation(targetDataSource)")
public void clearDataSource() {
DynamicDataSourceHolder.clear(); // 必須清理,防止線程池污染
}
}
@Service
public class UserService {
@Autowired UserDao userDao; // 僅注入一個 DAO
@TargetDataSource("dataSource_A")
public User getUser(Long id) { return userDao.findById(id); }
@TargetDataSource("dataSource_B")
public List getLogs(Long userId) { return logDao.findByUserId(userId); }
}
| 陷阱 | 後果 | 對策 |
|---|---|---|
| 跨數據源單事務 | 無法保證 ACID,JTA 複雜且效能差 | 業務層面拆分,最終一致性(補償/重試/消息隊列) |
| 線程池複用導致數據源洩漏 | 後續請求使用錯誤數據源 | @After 必須 clear(),配合 HikariCP 連接驗證 |
| 只讀副本路由衝突 | 寫操作誤路由到只讀庫 | 結合 @Transactional(readOnly=true) 判斷路由 |
Spring 多數據源配置的核心是「隔離與路由」。XML 顯式注入適合數據源固定、模塊清晰的場景;動態路由 + 註解驅動適合業務快速迭代、數據源頻繁變更的場景。選對模式,配合完善的測試與監控,才能駕馭多庫架構的複雜度。