SpringBoot 多數據源并存:原理、實踐與動態數據源辨析
引言
在復雜業務系統中,單一數據庫往往難以滿足所有需求:業務數據需要業務庫,管理數據需要理庫。此時讓不同業務對接專屬數據庫的多數據源并存方案,相比需要動態切換的模式,更能避免線程安全風險和事務混亂,成為更穩妥的選擇。
多數據源并存的底層邏輯
獨立組件的綁定
多數據源并存的本質是為每個數據庫創建一套完整的數據源 - 會話工廠 - 事務管理器獨立鏈路,各鏈路互不干擾,通過包路徑隔離實現業務與數據源的自動關聯。
關鍵組件關系:
- 一個數據庫 → 一個
DataSource(連接池實例) - 一個
DataSource→ 一個SqlSessionFactory(SQL會話工廠) - 一個
SqlSessionFactory→ 一個TransactionManager(事務管理器) - 一個事務管理器 → 綁定一組特定包下的
Mapper接口
與動態數據源的本質差異
很多開發者會混淆多數據源并存和動態數據源,兩者核心區別在于是否存在切換邏輯:
- 多數據源并存:無切換,各數據源長期存活,業務通過包路徑定向調用
- 動態數據源:有切換,通過
ThreadLocal存儲上下文,在同一鏈路中切換不同數據庫連接
簡單總結:多數據源是分工,動態數據源是兼職。
實現案例
多數據源配置
spring:
datasource:
druid:
# 業務庫配置
business:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/business?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: business
password: business
# 管理庫
management:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/management?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
username: management
password: management核心配置類:構建獨立組件鏈路
為每個數據源創建專屬配置類,通過注解指定掃描范圍和組件關聯,確保鏈路隔離。
@Configuration
@MapperScan(basePackages = "com.yian.mapper.business",sqlSessionFactoryRef = "busSqlSessionFactory")
public class BusDataSourceConfig {
// 業務庫數據源配置
@Bean(name = "busDataSource")
@Qualifier("busDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.business")
public DataSource busDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 業務庫JdbcTemplate
@Bean(name = "busJdbcTemplate")
public JdbcTemplate busJdbcTemplate(
@Qualifier("busDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}@Configuration
@MapperScan(basePackages = "com.yian.mapper.management",sqlSessionFactoryRef = "manSqlSessionFactory")
public class ManDataSourceConfig {
// 管理庫數據源配置
@Bean(name = "manDataSource")
@Qualifier("manDataSource")
@ConfigurationProperties(prefix = "spring.datasource.druid.management")
public DataSource manDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 管理庫JdbcTemplate
@Bean(name = "manJdbcTemplate")
public JdbcTemplate manJdbcTemplate(
@Qualifier("manDataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}@Configuration
public class MybatisPlusConfig {
@Bean
public SqlSessionFactory manSqlSessionFactory(@Qualifier("manDataSource") DataSource manDataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(manDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/management/**/*.xml"));
//map接收返回值值為null的問題,默認是當值為null,將key返回
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setCallSettersOnNulls(true);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
@Bean
public PlatformTransactionManager manTransactionManager(@Qualifier("manDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate manTransactionTemplate(@Qualifier("manTransactionManager") PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
@Bean
public SqlSessionFactory busSqlSessionFactory(@Qualifier("busDataSource") DataSource busDataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(busDataSource);
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sessionFactory.setMapperLocations(resolver.getResources("classpath*:/mapper/business/**/*.xml"));
//map接收返回值值為null的問題,默認是當值為null,將key返回
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setCallSettersOnNulls(true);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
@Bean
public PlatformTransactionManager busTransactionManager(@Qualifier("busDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate busTransactionTemplate(@Qualifier("busTransactionManager") PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}事務使用規范
單數據源事務
通過@Transactional(transactionManager = "xxxTransactionManager")指定事務管理器
跨數據源事務
方式一:編程式事務
public void createOrderWithMonitorLog(OrderDTO orderDTO) {
// 1. 定義事務屬性(默認:隔離級別DEFAULT,傳播行為REQUIRED)
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
// 2. 開啟兩個數據源的事務,獲取事務狀態(用于后續提交/回滾)
TransactionStatus busTxStatus = busTransactionManager.getTransaction(txDefinition);
TransactionStatus manTxStatus = manTransactionManager.getTransaction(txDefinition);
try {
// 3. 執行業務庫:插入訂單
orderMapper.insertOrder(orderDTO);
// 4. 執行管理庫:記錄訂單創建的監控日志
MonitorLog log = new MonitorLog()
.setBizType("ORDER_CREATE")
.setBizId(orderDTO.getOrderId())
.setOperateTime(System.currentTimeMillis());
monitorLogMapper.insertMonitorLog(log);
// 5. 無異常時,提交兩個數據源的事務(順序可調整,建議先提交非核心庫)
busTransactionManager.commit(busTxStatus);
manTransactionManager.commit(manTxStatus);
System.out.println("跨數據源事務執行成功,訂單創建+日志記錄完成");
} catch (Exception e) {
// 6. 有異常時,回滾所有事務
if (!busTxStatus.isRollbackOnly()) {
busTransactionManager.rollback(busTxStatus);
}
if (!manTxStatus.isRollbackOnly()) {
manTransactionManager.rollback(manTxStatus);
}
// 7. 記錄異常日志,必要時觸發補償機制(如訂單回滾后刪除已生成的日志)
System.err.println("跨數據源事務執行失敗,已回滾:" + e.getMessage());
throw new BusinessException("訂單創建失敗,請重試", e);
}
}方式二:分布式事務
@Service
public class OrderService {
// 其他注入代碼...
// 全局事務注解:rollbackFor指定異常回滾,timeout設置超時時間
@GlobalTransactional(rollbackFor = Exception.class, timeout = 60000)
public void createOrderWithMonitorLog(OrderDTO orderDTO) {
// 1. 操作MySQL:插入訂單(Seata自動記錄undo日志)
orderMapper.insertOrder(orderDTO);
// 2. 操作TDengine:記錄監控日志(Seata自動記錄undo日志)
MonitorLog log = new MonitorLog()
.setBizType("ORDER_CREATE")
.setBizId(orderDTO.getOrderId())
.setOperateTime(System.currentTimeMillis());
monitorLogMapper.insertMonitorLog(log);
// 若此處拋出異常,Seata會協調兩個數據源自動回滾;無異常則自動提交
}
}





























