动态数据源切换
多数据源架构允许应用在不同场景下路由到不同数据库,实现读写分离、跨库查询、租户隔离等需求。本文基于 AbstractRoutingDataSource 结合 MyBatis 实现完整的动态数据源方案。
核心原理
Java
数据源路由流程
┌─────────────┐
│ 业务代码 │
│ setRoute() │
└──────┬──────┘
│ ThreadLocal 存储路由键
↓
┌──────────────────────────┐
│ RoutingDataSource │
│ determineCurrentLookupKey│ ← 返回路由键 (master/slave/tenant_x)
└──────┬───────────────────┘
│ 查找已注册数据源
↓
┌──────────────────────────┐
│ DataSource Map │
│ master → HikariDS_1 │
│ slave1 → HikariDS_2 │
│ slave2 → HikariDS_3 │
└──────┬───────────────────┘
│ 返回实际 Connection
↓
┌─────────────┐
│ SQL 执行 │
└─────────────┘
AbstractRoutingDataSource 是 Spring 提供的多数据源抽象类,核心方法 determineCurrentLookupKey() 返回当前应使用的数据源键值。
读写分离实现
路由键定义
Java
public class DataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static final String MASTER = "master";
public static final String SLAVE = "slave";
public static void setDataSource(String ds) {
CONTEXT.set(ds);
}
public static String getDataSource() {
return CONTEXT.get() != null ? CONTEXT.get() : MASTER;
}
public static void clear() {
CONTEXT.remove();
}
}
路由数据源
Java
public class ReadWriteSplittingDataSource extends AbstractRoutingDataSource {
private int slaveCount = 0;
private final AtomicInteger counter = new AtomicInteger(0);
public ReadWriteSplittingDataSource(DataSource master, List<DataSource> slaves) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceContextHolder.MASTER, master);
for (int i = 0; i < slaves.size(); i++) {
targetDataSources.put(DataSourceContextHolder.SLAVE + i, slaves.get(i));
}
slaveCount = slaves.size();
setTargetDataSources(targetDataSources);
setDefaultTargetDataSource(master);
afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
String route = DataSourceContextHolder.getDataSource();
// 轮询选择 slave
if (DataSourceContextHolder.SLAVE.equals(route) && slaveCount > 0) {
int index = Math.abs(counter.getAndIncrement() % slaveCount);
return DataSourceContextHolder.SLAVE + index;
}
return DataSourceContextHolder.MASTER;
}
}
AOP 自动路由
YAML
@Aspect
@Component
@Order(-1) // 确保在事务切面之前执行
public class DataSourceAspect {
@Pointcut("execution(* com.example..mapper.*.*(..))")
public void mapperPointcut() {}
@Before("mapperPointcut()")
public void before(JoinPoint point) {
String methodName = point.getSignature().getName();
// 读操作自动路由到 slave
if (methodName.startsWith("select") || methodName.startsWith("find")
|| methodName.startsWith("get") || methodName.startsWith("list")
|| methodName.startsWith("count") || methodName.startsWith("query")) {
DataSourceContextHolder.setDataSource(DataSourceContextHolder.SLAVE);
} else {
// 写操作路由到 master
DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER);
}
}
@After("mapperPointcut()")
public void after() {
DataSourceContextHolder.clear();
}
}
Spring Boot 配置
Java
spring:
datasource:
master:
url: jdbc:mysql://master-host:3306/app_db
username: root
password: xxx
driver-class-name: com.mysql.cj.jdbc.Driver
slaves:
- url: jdbc:mysql://slave1-host:3306/app_db
username: read_user
password: xxx
- url: jdbc:mysql://slave2-host:3306/app_db
username: read_user
password: xxx
Java
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public List<DataSource> slaveDataSources(
@Value("${spring.datasource.slaves[0].url}") String slave1Url,
@Value("${spring.datasource.slaves[0].username}") String slave1User,
@Value("${spring.datasource.slaves[0].password}") String slave1Pwd,
@Value("${spring.datasource.slaves[1].url}") String slave2Url,
@Value("${spring.datasource.slaves[1].username}") String slave2User,
@Value("${spring.datasource.slaves[1].password}") String slave2Pwd) {
List<DataSource> slaves = new ArrayList<>();
slaves.add(buildDataSource(slave1Url, slave1User, slave1Pwd));
slaves.add(buildDataSource(slave2Url, slave2User, slave2Pwd));
return slaves;
}
private DataSource buildDataSource(String url, String user, String pwd) {
return DataSourceBuilder.create()
.url(url)
.username(user)
.password(pwd)
.build();
}
@Bean
public ReadWriteSplittingDataSource routingDataSource(
DataSource master, List<DataSource> slaves) {
return new ReadWriteSplittingDataSource(master, slaves);
}
@Bean
public SqlSessionFactory sqlSessionFactory(ReadWriteSplittingDataSource ds) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(ds);
factory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/**/*.xml"));
return factory.getObject();
}
}
手动指定数据源
自定义注解
Java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default DataSourceContextHolder.MASTER;
}
Java
@Aspect
@Component
@Order(-1)
public class ManualDataSourceAspect {
@Before("@annotation(dataSource)")
public void before(DataSource dataSource) {
DataSourceContextHolder.setDataSource(dataSource.value());
}
@After("@annotation(dataSource)")
public void after(DataSource dataSource) {
DataSourceContextHolder.clear();
}
}
Java
public interface OrderMapper {
// 自动路由到 slave(select 前缀)
List<Order> selectOrders();
// 手动指定数据源
@DataSource("slave")
Order selectFromBackup(@Param("id") Long id);
// 强制 master
@DataSource("master")
int forceMasterUpdate(@Param("id") Long id, @Param("status") String status);
}
事务内数据源一致性
读写分离场景中,同一事务内必须使用同一数据源,避免主从延迟导致的数据不一致。
Java
事务内数据源一致性
┌────────────────────────────────────┐
│ @Transactional │
│ select() → 从库 (旧数据) ❌ │
│ update() → 主库 (新数据) │
│ select() → 从库 (仍未更新) ❌ │
│ │
│ 正确做法:整个事务使用主库 │
│ update() → 主库 │
│ select() → 主库 (读到最新) ✓ │
└────────────────────────────────────┘
事务同步方案
Java
@Aspect
@Component
@Order(-1)
public class TransactionAwareDataSourceAspect {
@Before("@annotation(dataSource)")
public void before(DataSource dataSource) {
// 如果当前已在事务中,强制使用 master
if (TransactionSynchronizationManager.isActualTransactionActive()) {
DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER);
} else {
DataSourceContextHolder.setDataSource(dataSource.value());
}
}
@After("@annotation(dataSource)")
public void after(DataSource dataSource) {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
DataSourceContextHolder.clear();
}
}
}
同一事务内,Spring 复用同一 Connection。
AbstractRoutingDataSource在首次determineCurrentLookupKey()后缓存结果,事务内不会切换。
多数据源与 MyBatis 集成要点
SqlSessionTemplate 配置
Java
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
// ExecutorType.SIMPLE 适用于读写分离场景
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
}
多数据源独立 SqlSessionFactory
当不同数据源需要完全不同的 Mapper 配置时,可创建多个独立的 SqlSessionFactory:
text
@Configuration
public class MultiDataSourceConfig {
@Bean(name = "orderSqlSessionFactory")
public SqlSessionFactory orderSqlSessionFactory(
@Qualifier("orderDataSource") DataSource ds) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(ds);
factory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/order/**/*.xml"));
return factory.getObject();
}
@Bean(name = "userSqlSessionFactory")
public SqlSessionFactory userSqlSessionFactory(
@Qualifier("userDataSource") DataSource ds) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(ds);
factory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/user/**/*.xml"));
return factory.getObject();
}
}
动态数据源监控
text
@Component
public class DataSourceMonitor {
@Autowired
private ReadWriteSplittingDataSource routingDs;
/** 获取当前线程使用的数据源 */
public String getCurrentDataSource() {
return DataSourceContextHolder.getDataSource();
}
/** 获取所有已注册数据源 */
public Map<Object, Object> getRegisteredDataSources() {
return routingDs.getResolvedDataSources();
}
/** 切换记录 */
public void logSwitch(String from, String to) {
// 接入监控系统:Prometheus / SkyWalking
}
}
注意事项
- 主从延迟:写操作后立即读取可能读到旧数据,重要场景强制读主库
- 事务内不切换:同一事务内必须使用同一数据源,Spring 会自动复用 Connection
- AOP 顺序:数据源切换 AOP 的
@Order必须小于事务 AOP 的@Order,确保先切换数据源再开事务- 连接池独立:主从库应使用独立的连接池配置,slave 可使用只读用户降低权限
- ThreadLocal 清理:请求结束后必须在
finally或@After中清理 ThreadLocal,防止线程复用导致数据源泄漏
要点总结
AbstractRoutingDataSource通过determineCurrentLookupKey()返回路由键,动态选择目标数据源- 读写分离通过 AOP 根据方法名前缀自动路由:select/find/get → slave,update/insert/delete → master
- 多 slave 场景可使用轮询策略实现读负载均衡
- 事务内必须保证数据源一致性,通过
TransactionSynchronizationManager.isActualTransactionActive()判断 - 手动指定数据源使用自定义
@DataSource注解,通过 AOP 在方法执行前设置路由键 - 多套独立 Mapper 需创建多个
SqlSessionFactory,各自绑定不同数据源和 XML 路径 - ThreadLocal 必须在请求结束后清理,AOP 顺序必须在事务切面之前
存放路径:D:\git2\jwdev\articles\MYBATIS\专家\多租户与数据权限\动态数据源切换.md
📝 发现内容有误?点击此处直接编辑