mybatis-plus动态数据源切换问题

多数据源问题

问题:动态添加的数据源,在Service层调用时候,不能切换到动态加入的数据源上。

一个静态主库(MYSQL),多个动态项目库(SQLITE)

一个主库(MYSQL)的配置在yaml中,配置如下

spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        dynamic:
            primary: master
            strict: false
            datasource:
                master:
                    url: jdbc:mysql://localhost:3306/kdss_new_system?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                    username: root
                    password: Abcd1234
                    driverClassName: com.mysql.cj.jdbc.Driver

多个项目库为手动方式,在请求项目数据时,动态添加当前项目的数据库(SQLITE),代码如下

@Autowired
private DataSource dataSource;

@Autowired
private DruidDataSourceCreator druidDataSourceCreator;
    
public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) {
        DataSourceProperty dataSourceProperty = new DataSourceProperty();
        BeanUtils.copyProperties(dto, dataSourceProperty);
        dataSourceProperty.setLazy(true);
        DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
        DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
        ds.addDataSource(dto.getPoolName(), dataSource);
        return ds.getDataSources().keySet();
    }
    

在Service层对DS进行标识,代码如下

@Service
@DS("#project")
public class FileSpaceConfigServiceImpl extends BaseLogicServiceImpl<FileSpaceConfigMapper,FileSpaceConfig> implements IFileSpaceConfigService
{
    /** 路径是否唯一的返回结果码 */
    public final static String PATH_UNIQUE = "0";
    public final static String PATH_NOT_UNIQUE = "1";

    /**
     * 根据条件查找数据
     * @param fileSpaceConfig 条件
     * @return 符合条件的数据
     */
    @Override
    public List<FileSpaceConfig> selectList(FileSpaceConfig fileSpaceConfig) {
        fileSpaceConfig.setFlag(0);
        return getBaseMapper().selectAll(fileSpaceConfig);
    }
}

Controller层调用的时候,设置上下文的值为项目编号

ProjectDataSourceContextHolder.setDataSourceType(poolName);
//上下文环境定义如下:
public class ProjectDataSourceContextHolder
{
    public static final Logger log = LoggerFactory.getLogger(ProjectDataSourceContextHolder.class);

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String projectId)
    {
        log.info("切换到{}数据源", projectId);
        CONTEXT_HOLDER.set(projectId);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType()
    {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}

重写DsProcessor,如下:

public class DsHeaderProcessor extends DsProcessor {
    private static final String HEADER_PREFIX = "#project";

    @Override
    public boolean matches(String key) {
        return key.equals(HEADER_PREFIX);
    }

    @Override
    public String doDetermineDatasource(MethodInvocation invocation, String key) {
        if(key.equals(HEADER_PREFIX)){
            return ProjectDataSourceContextHolder.getDataSourceType();
        }else{
            return key;
        }
    }
}

定义配置的使用场景

@Configuration
public class CustomerDynamicDataSourceConfig {

    @Bean
    public DsProcessor dsProcessor() {
        DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
        DsSessionProcessor sessionProcessor = new DsSessionProcessor();
        DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
        headerProcessor.setNextProcessor(sessionProcessor);
        sessionProcessor.setNextProcessor(spelExpressionProcessor);
        return headerProcessor;
    }
}

【以下回答由 GPT 生成】

思路: 1. 在调用Service方法之前,通过切面拦截器动态设置数据源。 2. 在切面拦截器中实现动态切换数据源的逻辑。

具体步骤如下:

  1. 创建一个切面拦截器类,用于动态切换数据源。
@Aspect
@Component
public class DataSourceAspect {

    @Pointcut("@annotation(com.baomidou.dynamic.datasource.annotation.DS)")
    public void dataSourcePointCut() {
    }

    @Before("dataSourcePointCut() && @annotation(ds)")
    public void switchDataSource(JoinPoint joinPoint, DS ds) {
        String value = ds.value();
        DynamicDataSourceContextHolder.setDataSource(value);
    }

    @After("dataSourcePointCut()")
    public void restoreDataSource(JoinPoint joinPoint) {
        DynamicDataSourceContextHolder.clearDataSource();
    }
}

该切面拦截器会拦截带有@DS注解的方法,并根据注解中的值动态切换数据源。

  1. 创建一个数据源上下文类,用于保存当前线程使用的数据源的标识。
public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSource(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

该类使用ThreadLocal保存当前线程使用的数据源的标识,在切换数据源时设置数据源标识,在方法执行完成后清除数据源标识。

  1. 修改配置类,将切面拦截器和数据源上下文类添加到Spring容器中。
@Configuration
public class DataSourceConfig {

    @Autowired
    private List<DataSource> dataSources;

    @Bean
    public DataSourceRouter dataSourceRouter() {
        DataSourceRouter router = new DataSourceRouter();
        router.setTargetDataSources(dataSources.stream()
                .collect(Collectors.toMap(DataSource::getName, Function.identity())));
        router.setDefaultTargetDataSource(dataSources.get(0));
        return router;
    }

    @Primary
    @Bean(name = "dataSource")
    public DynamicRoutingDataSource dynamicRoutingDataSource(DataSourceRouter dataSourceRouter) {
        DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
        dynamicRoutingDataSource.setDataSourceLookup(new MapDataSourceLookup(dataSourceRouter.getDataSources()));
        dynamicRoutingDataSource.setDefaultDataSource(dataSourceRouter.getDefaultTargetDataSource());
        return dynamicRoutingDataSource;
    }

    @Bean
    public DataSourceAspect dataSourceAspect() {
        return new DataSourceAspect();
    }

    @Bean
    public DynamicDataSourceContextHolder dynamicDataSourceContextHolder() {
        return new DynamicDataSourceContextHolder();
    }
}

这里主要是配置了 DataSourceRouterDynamicRoutingDataSource,前者用于管理多个数据源,后者用于动态切换数据源。

  1. 在Service层方法上添加@DS注解,指定使用的数据源。
@Service
public class FileSpaceConfigServiceImpl extends BaseLogicServiceImpl<FileSpaceConfigMapper, FileSpaceConfig> implements IFileSpaceConfigService {

    @DS("#project")
    public void doSomething() {
        ...
    }
}

通过@DS("#project")注解来指定使用的数据源,其中#project可以是一个表达式,动态的从方法的参数、类属性等地方获取数据源的标识。

  1. 在调用Service方法时,动态切换数据源。
@Autowired
private IFileSpaceConfigService fileSpaceConfigService;

public void invokeServiceMethod(String project) {
    DynamicDataSourceContextHolder.setDataSource(project);
    fileSpaceConfigService.doSomething();
    DynamicDataSourceContextHolder.clearDataSource();
}

在调用Service方法之前,使用DynamicDataSourceContextHolder.setDataSource(project)来设置数据源的标识,在调用完成后使用DynamicDataSourceContextHolder.clearDataSource()来清除数据源的标识。

这样就解决了mybatis-plus动态数据源切换的问题。

注:以上是一种思路和解决方案,具体实现可能需要根据项目的具体情况进行调整。


如果你已经解决了该问题, 非常希望你能够分享一下解决方案, 写成博客, 将相关链接放在评论区, 以帮助更多的人 ^-^