Fork me on GitHub

Spring Boot +Mybatis plus多数据源实践

  随着业务及客户的不断壮大,单数据库已经不足以支撑程序业务的完美运行(响应快、高吞吐),所以数据库往往都会进行分表分库/读写分离,那么问题来了,分库后程序如何从不同URL数据库中读取数据呢?

  这篇文章只讲如何配置/使用多数据源,不讲分表分库/读写分离,也不讲主键生成策略及读取策略。

  如何实现多数据源呢?原理很简单:Spring的AOP.只需要mybatis plus及spring boot的基础依赖,不需要引入其他依赖

  说明:多数据源不仅指同类不同地址的数据源,也可以是异构关系型数据库

  自定义注解

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

    enum DataSourceType {
        /**
         * 数据源类型
         **/
        MASTER,
        SLAVE,
    }
}

yml文件

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8
  datasource:
    driver-class-name: org.postgresql.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 10
    db:
      conn:
        str: useUnicode=true&characterEncoding=UTF-8
    master:
      jdbc-url: jdbc:postgresql://127.0.01:54320/master?${spring.datasource.db.conn.str}
      username: postgres
      password: 123456
    salve:
      jdbc-url: jdbc:postgresql://127.0.0.1:54321/salve?${spring.datasource.db.conn.str}
      username: postgres
      password: 123456

动态数据源上下文

@Slf4j
public class DynamicDataSourceContextHolder {

    /**
     * 数据源标识,保存在线程变量中,避免多线程操作数据源时互相干扰
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     *
     * @param dataSource 数据源名称
     */
    public static void setDataSource(String dataSource) {
        log.info("切换到{}数据源", Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource);
        CONTEXT_HOLDER.set(Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource);
    }

    /**
     * 获取数据源
     *
     * @return java.lang.String
     * @author Jackpot
     * @date 2021/4/30 4:38 下午
     */
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }

}

动态数据源

继承AbstractRoutingDataSource类,该类是能够实现数据源切换的关键所在。实现determineCurrentLookupKey(),返回数据源的key值。

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSource = DynamicDataSourceContextHolder.getDataSource();
        log.info("当前数据源:{}", Assert.isEmpty(dataSource) ?
                com.**.config.datasource.DataSource.DataSourceType.MASTE.name() : dataSource);
        return dataSource;
    }

}

多数据源配置

@Slf4j
@Configuration
public class DataSourceConfig {

    final MybatisPlusInterceptor interceptor;

    public DataSourceConfig(MybatisPlusInterceptor interceptor) {
        this.interceptor = interceptor;
    }

    /**
     * 主数据源配置
     * Primary 表示当前数据源为主数据源
     *
     * @return javax.sql.DataSource
     * @author Jackpot
     * @date 2021/5/12 9:32 上午
     */
    @Primary
    @Bean("master")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 从数据源配置
     *
     * @return javax.sql.DataSource
     * @author Jackpot
     * @date 2021/5/12 9:32 上午
     */
    @Bean("salve")
    @ConfigurationProperties(prefix = "spring.datasource.salve")
    public DataSource salveDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 多数据源定义
     *
     * @param masterDataSource  主数据源
     * @param salveDataSource 从数据源
     * @return com.**.config.datasource.DynamicDataSource
     * @author Jackpot
     * @date 2021/5/12 9:33 上午
     */
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier("master") DataSource meshDataSource, @Qualifier("salve") DataSource walleDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(4);
        targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.MASTER.name(), masterDataSource);
        targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.SALVE.name(), salveDataSource);
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 将动态数据源注入到SqlSessionFactory
     * 同时解决分页失效
     *
     * @param dynamicDataSource 多数据源
     * @return org.apache.ibatis.session.SqlSessionFactory
     * @author Jackpot
     * @date 2021/4/30 4:30 下午
     */
    @Bean("sqlSessionFactory")
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        final MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        //分页插件
        bean.setPlugins(interceptor);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*Mapper.xml"));
        return bean.getObject();
    }

    /**
     * sqlSessionTemplate定义
     *
     * @param sessionFactory session工厂
     * @return org.mybatis.spring.SqlSessionTemplate
     * @author Jackpot
     * @date 2021/5/12 9:34 上午
     */
    @Bean("sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sessionFactory) {
        return new SqlSessionTemplate(sessionFactory);
    }

    /**
     * 多数据源事务管理
     * 防止事务绑定主数据源无法对数据源进行切换及事务失效
     *
     * @param dataSource 多数据源
     * @return org.springframework.jdbc.datasource.DataSourceTransactionManager
     * @author Jackpot
     * @date 2021/5/12 9:39 上午
     */
    @Bean("dataSourceTransactionManager")
    public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

}

AOP切面实现。

注:@annotation和@within怎么区分?通俗点说就是@annotation作用与方法(method)之上,@within作用于controller/service上,也就是对象

@Slf4j
@Order(0)
@Aspect
@Component
public class DataSourceAspect {

    /**
     * 通过自定义注解@DataSource定义切点
     */
    @Pointcut("@annotation(com.**.config.datasource.DataSource)" + "|| @within(com.**.config.datasource.DataSource)")
    public void dsPointCut() {
    }

    /**
     * 切点环绕
     *
     * @param point 切入点
     * @return java.lang.Object
     * @author Jackpot
     * @date 2021/4/30 4:34 下午
     */
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (dataSource != null) {
            DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearDataSource();
        }
    }

    /**
     * 获取需要切换的数据源
     *
     * @param point 切入点
     * @return com.iot.mesh.common.config.datasource.DataSource
     * @author Jackpot
     * @date 2021/4/30 4:34 下午
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<?> targetClass = point.getTarget().getClass();
        DataSource targetDataSource = targetClass.getAnnotation(DataSource.class);
        if (targetDataSource != null) {
            return targetDataSource;
        } else {
            Method method = signature.getMethod();
            return method.getAnnotation(DataSource.class);
        }
    }
}

启动类,需要剔除spring boot的自动数据源配置

@EnableScheduling
@EnableTransactionManagement
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class ApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }


}

使用:粗粒度使用在service类对象之上,细粒度作用于方法之上,同时存在方法注解优先于类上注解。推荐使用第二种方式。

注:同一个方法里面如果涉及到多个数据源操作,事务会失效。

 

 

 附:其实mybatis plus提供了多数据的依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>

 

 

 想偷懒的同学可以直接使用,上面主要讲的是核心内容,dynamic-datasource-spring-boot-starter实现动态数据源的思想也是AOP,把上面的代码弄懂了,dynamic-datasource的源码看起来也就很简单了.

  mybatis plus的多数据源的使用也很简单,对象或方法上使用@DS(**)即可。

  注:使用mybatis plus动态数据源,yml文件格式和上面有点区别,我这里也把它贴出来。启动类上也不需要将DataSourceAutoConfiguration.class剔除

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    locale: zh_CN
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值为master
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      datasource:
        master:
          url: jdbc:postgresql://127.0.0.1:54320/master?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true
          username: postgres
          password: 123456
        salve:
          url: jdbc:postgresql://127.0.0.1:54321/salve?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true
          username: postgres
          password: 123456

 

posted @ 2022-08-24 12:12  JackpotHan  阅读(1483)  评论(0编辑  收藏  举报