在復雜業務系統中,多數據源切換已成為必備技能。本文將深入剖析三種主流實現方案,帶你從入門到精通!
一、多數據源應用場景
-
讀寫分離:主庫負責寫操作,從庫處理讀請求
-
多租戶系統:不同租戶使用獨立數據庫
-
分庫分表:業務數據按規則分散存儲
-
多數據庫類型:同時使用MySQL、Oracle等異構數據庫
二、3種實現方案對比
方案 | 實現復雜度 | 侵入性 | 維護成本 | 適用場景 |
---|---|---|---|---|
AbstractRoutingDataSource | 中等 | 高 | 高 | 簡單讀寫分離 |
多SqlSessionFactory | 高 | 高 | 高 | 異構數據庫 |
dynamic-datasource | 低 | 低 | 低 | 復雜多數據源 |
三、方案一:AbstractRoutingDataSource
實現原理
通過繼承Spring的AbstractRoutingDataSource
類,動態路由數據源
實現步驟
1. 添加依賴
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.27</version></dependency>
</dependencies>
?
2. 配置多數據源
# application.yml
spring:datasource:master:driver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/master_dbusername: rootpassword: root123slave:driver-class-name: com.mysql.cj.jdbc.Driverjdbc-url: jdbc:mysql://localhost:3306/slave_dbusername: rootpassword: root123
3. 動態數據源配置類
@Configuration
public class DataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}@Beanpublic DataSource dynamicDataSource() {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("master", masterDataSource());targetDataSources.put("slave", slaveDataSource());DynamicDataSource dynamicDataSource = new DynamicDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource());return dynamicDataSource;}
}
4. 自定義動態數據源
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DataSourceContextHolder.getDataSourceKey();}public static class DataSourceContextHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setDataSourceKey(String key) {contextHolder.set(key);}public static String getDataSourceKey() {return contextHolder.get();}public static void clearDataSourceKey() {contextHolder.remove();}}
}
5. 自定義注解切換數據源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String value() default "master";
}
6. AOP切面實現動態切換
@Aspect
@Component
public class DataSourceAspect {@Before("@annotation(dataSource)")public void beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {String dataSourceKey = dataSource.value();DynamicDataSource.DataSourceContextHolder.setDataSourceKey(dataSourceKey);}@After("@annotation(dataSource)")public void afterSwitchDataSource(JoinPoint point, DataSource dataSource) {DynamicDataSource.DataSourceContextHolder.clearDataSourceKey();}
}
7. 業務層使用示例
@Service
public class UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;// 使用主庫@DataSource("master")public void createUser(User user) {String sql = "INSERT INTO users(name, email) VALUES(?, ?)";jdbcTemplate.update(sql, user.getName(), user.getEmail());}// 使用從庫@DataSource("slave")public List<User> getAllUsers() {String sql = "SELECT * FROM users";return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));}
}
方案優缺點
優點:
-
純Spring實現,無第三方依賴
-
靈活控制數據源切換
缺點:
-
事務管理復雜
-
需手動處理連接池
-
切換邏輯侵入業務代碼
四、方案二:多SqlSessionFactory
實現原理
為每個數據源創建獨立的MyBatis SqlSessionFactory
實現步驟
1. 主數據源配置
@Configuration
@MapperScan(basePackages = "com.example.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Beanpublic SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/master/*.xml"));return bean.getObject();}@Beanpublic DataSourceTransactionManager masterTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
2. 從數據源配置
@Configuration
@MapperScan(basePackages = "com.example.mapper.slave", sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.slave")public DataSource slaveDataSource() {return DataSourceBuilder.create().build();}@Beanpublic SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(dataSource);bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/slave/*.xml"));return bean.getObject();}@Beanpublic DataSourceTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
3. 業務層使用
@Service
public class OrderService {// 注入主庫Mapper@Autowired@Qualifier("masterOrderMapper")private OrderMapper masterOrderMapper;// 注入從庫Mapper@Autowired@Qualifier("slaveOrderMapper")private OrderMapper slaveOrderMapper;@Transactional(transactionManager = "masterTransactionManager")public void createOrder(Order order) {masterOrderMapper.insert(order);}@Transactional(transactionManager = "slaveTransactionManager")public Order getOrder(Long id) {return slaveOrderMapper.selectById(id);}
}
方案優缺點
優點:
-
各數據源完全隔離
-
事務管理清晰
-
支持異構數據庫
缺點:
-
配置復雜,冗余代碼多
-
Mapper需按數據源分包
-
動態切換不靈活
五、方案三:dynamic-datasource框架
框架優勢
-
零侵入:通過注解實現數據源切換
-
功能豐富:支持讀寫分離、分庫分表等
-
簡單易用:簡化多數據源配置
實現步驟
1. 添加依賴
<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.0</version>
</dependency>
2. 配置數據源
spring:datasource:dynamic:primary: master # 默認數據源strict: false # 是否嚴格匹配數據源datasource:master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/master_dbusername: rootpassword: root123slave1:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/slave_db1username: rootpassword: root123slave2:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/slave_db2username: rootpassword: root123oracle_db:driver-class-name: oracle.jdbc.OracleDriverurl: jdbc:oracle:thin:@localhost:1521:orclusername: systempassword: oracle123
3. 使用@DS注解切換數據源
@Service
public class ProductService {// 默認使用主庫@Autowiredprivate JdbcTemplate jdbcTemplate;// 使用主庫@DS("master")public void createProduct(Product product) {jdbcTemplate.update("INSERT INTO product(...) VALUES(...)");}// 隨機使用從庫@DS("slave")public Product getProduct(Long id) {return jdbcTemplate.queryForObject("SELECT * FROM product WHERE id = ?", new BeanPropertyRowMapper<>(Product.class), id);}// 指定特定從庫@DS("slave1")public List<Product> getHotProducts() {return jdbcTemplate.query("SELECT * FROM product WHERE hot = 1", new BeanPropertyRowMapper<>(Product.class));}// 使用Oracle數據庫@DS("oracle_db")public List<Category> getOracleCategories() {return jdbcTemplate.query("SELECT * FROM categories", new BeanPropertyRowMapper<>(Category.class));}
}
4. 高級功能:讀寫分離
spring:datasource:dynamic:primary: masterdatasource:master:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://master-host:3306/dbusername: rootpassword: root123slave_1:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://slave1-host:3306/dbusername: rootpassword: root123slave_2:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://slave2-host:3306/dbusername: rootpassword: root123strategy: # 讀寫分離配置load-balance: # 負載均衡策略slave: round_robin # 從庫輪詢策略
5. 事務管理
@DS("master")
@Transactional
public void placeOrder(Order order) {// 1. 扣減庫存productService.reduceStock(order.getProductId(), order.getQuantity());// 2. 創建訂單orderMapper.insert(order);// 3. 記錄日志logService.logOrder(order);// 所有操作都在主庫事務中執行
}
最佳實踐技巧
-
數據源分組管理:
spring:datasource:dynamic:datasource:master_1: # 配置...master_2:# 配置...slave_1:# 配置...slave_2:# 配置...group:masters: master_1, master_2slaves: slave_1, slave_2
-
多租戶數據源動態注冊:
@Autowired private DynamicRoutingDataSource routingDataSource;public void addTenantDataSource(String tenantId, DataSourceProperty property) {DataSource dataSource = dataSourceCreator.createDataSource(property);routingDataSource.addDataSource(tenantId, dataSource); }
-
自定義負載均衡策略:
public class RandomStrategy implements LoadBalanceStrategy {@Overridepublic String determineDataSource(List<String> dataSourceNames) {Random random = new Random();return dataSourceNames.get(random.nextInt(dataSourceNames.size()));} }
六、性能優化建議
-
連接池配置優化:
spring:datasource:dynamic:datasource:master:# ...hikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000idle-timeout: 600000max-lifetime: 1800000
-
避免頻繁切換數據源:
-
將同一數據源操作集中處理
-
使用@DSTransactional管理跨庫事務
-
-
監控數據源狀態:
@RestController public class DataSourceMonitor {@Autowiredprivate DynamicRoutingDataSource routingDataSource;@GetMapping("/datasources")public Map<String, DataSource> listDataSources() {return routingDataSource.getDataSources();}@GetMapping("/datasources/stats")public Map<String, Object> getDataSourceStats() {Map<String, Object> stats = new HashMap<>();routingDataSource.getDataSources().forEach((key, ds) -> {if(ds instanceof HikariDataSource) {HikariDataSource hikari = (HikariDataSource) ds;HikariPoolMXBean pool = hikari.getHikariPoolMXBean();stats.put(key, Map.of("active", pool.getActiveConnections(),"idle", pool.getIdleConnections(),"total", pool.getTotalConnections()));}});return stats;} }
七、方案選型建議
-
中小型項目:優先選用dynamic-datasource,開發效率高
-
異構數據庫系統:選擇多SqlSessionFactory方案,隔離性好
-
需要高度定制:AbstractRoutingDataSource提供最大靈活性
-
云原生環境:dynamic-datasource + Seata分布式事務
八、常見問題解決方案
-
數據源切換失效:
-
檢查方法是否被AOP代理
-
確保@DS注解在public方法上
-
避免類內部方法調用
-
-
跨庫事務問題:
// 使用分布式事務 @DS("order") @GlobalTransactional public void createOrder(Order order) {// 操作訂單庫orderMapper.insert(order);// 操作庫存庫stockService.reduce(order.getProductId(), order.getQuantity()); }
-
連接泄露檢測:
@Bean public DataSource dataSource(DataSourceProperties properties) {HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();dataSource.setLeakDetectionThreshold(5000); // 5秒泄露檢測return dataSource; }
九、結語
多數據源管理是現代應用開發的必備技能。通過本文介紹的三種方案:
-
AbstractRoutingDataSource:Spring原生方案,適合定制化場景
-
多SqlSessionFactory:適合異構數據庫系統
-
dynamic-datasource:生產環境首選,功能強大易用
最佳實踐提示:對于大多數Java項目,推薦使用dynamic-datasource框架,它提供了一站式的解決方案,大大降低了多數據源管理的復雜度。同時結合Spring Cloud Alibaba Seata,可輕松實現分布式事務管理。
擴展閱讀:
-
Spring官方文檔:數據訪問
-
dynamic-datasource高級用法
-
MyBatis多數據源最佳實踐
掌握多數據源切換技術,讓你的應用從容應對復雜數據場景!
?