JAVA:MyBatis 如何实现动态数据源切换的技术指南

admin
10
2025-08-25

1、简述

在实际的业务场景中,我们经常会遇到以下需求:

🔹 读写分离:写操作走主库,读操作走从库。

🔹 多租户系统:不同租户对应不同数据库。

🔹 分库分表场景:根据业务 ID 动态路由到不同的数据源。

Spring Boot + MyBatis 可以结合 AbstractRoutingDataSource 实现动态数据源切换。本文将结合一个 主从数据库读写分离 的案例,演示 MyBatis 动态数据源的实现。

image-hvlf.png


2、核心思路

🔹 定义一个 动态数据源上下文,保存当前线程使用的数据源标识(ThreadLocal)。

🔹 自定义一个继承 AbstractRoutingDataSource 的类,实现数据源路由逻辑。

🔹 配置多个真实数据源(如 master、slave)。

🔹 通过 AOP 或注解,在方法执行前设置数据源上下文。

🔹 MyBatis 查询时自动路由到对应数据源。


3、实践案例

3.1 定义数据源标识

public enum DataSourceType {
    MASTER, SLAVE
}

3.2 动态数据源上下文

public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>();

    public static void set(DataSourceType type) {
        CONTEXT.set(type);
    }

    public static DataSourceType get() {
        return CONTEXT.get() == null ? DataSourceType.MASTER : CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

3.3 动态数据源实现类

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

3.4 配置数据源

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class DataSourceConfig {

    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/master_db")
                .username("root")
                .password("123456")
                .build();
    }

    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/slave_db")
                .username("root")
                .password("123456")
                .build();
    }

    @Bean
    public DynamicDataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);

        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);

        return dynamicDataSource;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

3.5 定义注解

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceType value() default DataSourceType.MASTER;
}

3.6 AOP 切换数据源

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    @Around("@annotation(dataSource)")
    public Object switchDataSource(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        try {
            DataSourceContextHolder.set(dataSource.value());
            return point.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

3.7 MyBatis Mapper 示例

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;

@Mapper
public interface UserMapper {

    @Select("SELECT id, username FROM users")
    List<User> findAll();
}

3.8 Service 层

import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @DataSource(DataSourceType.MASTER)
    public List<User> queryFromMaster() {
        return userMapper.findAll();
    }

    @DataSource(DataSourceType.SLAVE)
    public List<User> queryFromSlave() {
        return userMapper.findAll();
    }
}

3.9 Controller 测试

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController
public class UserController {

    private final UserService userService;
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/master")
    public List<User> master() {
        return userService.queryFromMaster();
    }

    @GetMapping("/slave")
    public List<User> slave() {
        return userService.queryFromSlave();
    }
}

访问 /master/slave 时,会分别从不同数据库获取数据。


4、总结

🔹 通过 AbstractRoutingDataSource + ThreadLocal,可以灵活实现动态数据源切换。

🔹 可以结合 注解 + AOP 来简化调用。

🔹 常见应用场景:

  • 读写分离
  • 多租户数据库
  • 分库分表路由
动物装饰