AbstractRoutingDataSource+Mybatis拦截器实现动态切换数据源

AbstractRoutingDataSource+Mybatis拦截器实现动态切换数据源

场景:线上4个MySQL实例分属4个机器,每个实例8个库。根据某一业务id,能够唯一确定数据存放在某个机器的某个库下。
要求:执行sql的时候,根据业务id切换数据源
工具:AbstractRoutingDataSource 切换数据源+Mybatis拦截器切换库

1.数据源配置

yml配置文件参考如下:

dlcms:
  dataSource:
    dlcms:
      url: jdbc:mysql://${MYSQL_URL_1}:${MYSQL_PORT}/dlcms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowLoadLocalInfile=true&serverTimezone=Asia/Shanghai
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: ${DB_USER}
      password: ${DB_PASSWORD}
    dlcms_01:
      url: jdbc:mysql://${MYSQL_URL_2}:${MYSQL_PORT}/dlcms_01?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowLoadLocalInfile=true&serverTimezone=Asia/Shanghai
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: ${DB_USER}
      password: ${DB_PASSWORD}
......

MYSQL_URL_1、MYSQL_URL_2是不同的ip,表示MySQL分属不同机器。

2.读取数据源配置

@Component
@ConfigurationProperties(prefix = "dlcms")
public class DlcmsDataSourceProfile {
    private Map<String, DataSourceProperties> datasource;

    public Map<String, DataSourceProperties> getDatasource() {
        return datasource;
    }

    public void setDatasource(Map<String, DataSourceProperties> datasource) {
        this.datasource = datasource;
    }
}

SpringBoot启动时会加载配置,将以dlcms 开头的配置存放进 datasource 同名Map<String, DataSourceProperties>集合

https://i.imgtg.com/2023/02/15/dzA9S.png

datasource集合的key就是配置里的dlcms、dlcms_01等,value就是数据源信息,包括url、账号密码等。

3.继承AbstractRoutingDataSource 重写determineCurrentLookupKey

@Slf4j
@Component
public class DlcmsRoutingDataSource extends AbstractRoutingDataSource {
    public DlcmsRoutingDataSource(DlcmsDataSourceProfile profile) {
        Map<String, DataSourceProperties> datasource = profile.getDatasource();
        if (ObjectUtils.isEmpty(datasource)) {
            throw new BizException("数据源加载为空");
        }
        Map<Object, Object> map = datasource.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        e -> e.getValue().initializeDataSourceBuilder().type(DruidDataSource.class).build()
                ));
        setTargetDataSources(map);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        HospitalDbMap current = DbRouteUtil.getCurrent();
        if (ObjectUtils.isEmpty(current)) {
            throw new BizException("当前数据映射为空");
        }
        return current.sourceName();
    }
}

DlcmsRoutingDataSource 构造函数获取2步骤得到的数据源配置,进行格式转换,调用setTargetDataSources(map) 指定数据源映射。

https://i.imgtg.com/2023/02/15/dzZUN.png

可以理解为将配置文件中的数据源映射交给AbstractRoutingDataSource 管理。

切换数据源的关键在于重写的determineCurrentLookupKey() 方法。

https://i.imgtg.com/2023/02/15/dzvvC.png

该方法确定了下一次调用的sql的数据源是哪个,在每次执行sql前触发该方法,determineCurrentLookupKey() 方法返回值就是构造方法中设置的数据源的key,以此获取当前数据源。

4.数据源路由信息应当保存到ThreadLocal

由第3步知,要路由到哪个数据源,只需要告诉determineCurrentLookupKey() 方法当前的数据源key就行了。这个key一般叫做上下文(Context)。

上下文应当由ThreadLocal 容器存放。

https://i.imgtg.com/2023/02/15/dzPIx.png

ThreadLocal 保证数据只属于当前线程,防止并发时数据被错乱读取。

如下,构建工具类:

@Slf4j
@Component
public class DbRouteUtil {

    private static final ThreadLocal<HospitalDbMap> _alias = new ThreadLocal<>();

......

其中HospitalDbMap为:

public record HospitalDbMap(
        String hospitalCode,
        DataSouceEnum dataSouceEnum,
        String sourceName,
        String ylDatabase,
        String plgDatabase
) {

}

存放着一些与数据源相关的信息,其中sourceName可确定当前数据源。

5.Mybatis拦截器进行切库

AbstractRoutingDataSource 可以根据自定义条件动态路由某数据源,由于业务背景,数据分库存储,所以还需要进行切库操作。

在书写sql的时候,每次指明库名很麻烦,所以需要能够自动切换库。又因为业务数据的库可以根据某业务id确定,所以可以利用Mybatis拦截器,在执行sql前根据该业务id进行切库,sql就不再指明库名。

实现Interceptor 接口,重写intercept 方法:

@Component
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
@Slf4j
public class MybatisInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        if (target instanceof StatementHandler) {
            Connection connection = (Connection) invocation.getArgs()[0];
            HospitalDbMap current = DbRouteUtil.getCurrent();
            DataSouceEnum dataSouceEnum = current.dataSouceEnum();
            if (dataSouceEnum == null) {
                log.error("切换数据源,必须指定数据源类型");
                throw new BizException("切换数据源未指定数据源类型");
            }
            switch (dataSouceEnum) {
                case yueli -> connection.setCatalog(current.ylDatabase());
                case plg -> connection.setCatalog(current.plgDatabase());
                case dlcms -> connection.setCatalog(DataSouceEnum.dlcms.name());
                default -> log.debug("拦截到其他类型,dataSouceEnum={}", dataSouceEnum);
            }
        }
        return invocation.proceed();
    }
}

进入@Intercepts查看:

https://i.imgtg.com/2023/02/15/dzydL.png

  • @Intercepts 用于指明要在哪拦截的目标方法。
  • invocation.proceed() 就是被拦截的sql执行,在该方法前后可自定义书写内容,比如我这里修改当前数据源连接的库名。

@Signature 下包含了3个标签:

@Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
  • type:就是指定拦截器类型(ParameterHandler ,StatementHandler,ResultSetHandler)
  • method:拦截器类型中的方法,不是自己写的方法
  • args:是method中方法的入参

点进StatementHandler 查看:

https://i.imgtg.com/2023/02/15/dzpNX.png

在这里面选取method及设置对应args。

6.使用

https://i.imgtg.com/2023/02/15/dz4Rp.png

利用 AbstractRoutingDataSource 动态路由数据源,需要设置上下文(可以理解为某个特定数据源的标志,以此决定用哪个数据源)

我这里提取了一个工具类,上下文信息保存在ThreadLocal中,确保线程之间隔离。
在每次执行不同数据源的sql之前,需要手动执行切换数据源,在调用sql前,mybatis拦截器自动切换库名。

如:

https://i.imgtg.com/2023/02/15/dzJEt.png

提取工具类,手动进行数据源的切换。

7.总结

  • AbstractRoutingDataSource :切换数据源
  • ThreadLocal :保存上下文(数据源标记信息)
  • Mybatis拦截器:根据上下文自动切换库
  • 数据源路由工具类:方便手动执行切换数据源

参考资料

https://www.baeldung.com/spring-abstract-routing-data-source

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值