Springboot + Mybatis-plus读写分离

实现原理

  1. 基于Mybatis-plus的InnerInterceptor拦截器,区分查询还是增删改
  2. 基于Mybatis的AbstractRoutingDataSource,动态切换数据源

代码

  1. yaml
#服务器端口
server:
  port: 38103
spring:
  datasource:
    druid:
      initial-size: 5
      max-active: 20
      min-idle: 5
      max-wait: 60000
      # MySqlPostgreSQL校验
      validation-query: select 1
      # Oracle校验
      #validation-query: select 1 from dual
      validation-query-timeout: 2000
      test-on-borrow: false
      test-on-return: false
      test-while-idle: false
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: admin
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*'
        session-stat-enable: true
        session-stat-max-count: 10
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://xxx:xxx/xxx?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
      username: root
      password: xxx
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://xxx:xxx/xxx?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
      username: root
      password: xxx
  1. 数据源配置
@AutoConfigureBefore(DruidDataSourceAutoConfigure.class)
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Data
public class DataSourceConfig {

    private int initialSize;
    private int maxActive;
    private int minIdle;
    private long maxWait;
    private long minEvictableIdleTimeMillis;
    private long timeBetweenEvictionRunsMillis;
    private boolean testWhileIdle;

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        parseDruidConfig(dataSource);
        return dataSource;
    }

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        parseDruidConfig(dataSource);
        return dataSource;
    }

    private void parseDruidConfig(DruidDataSource dataSource) {
        dataSource.setInitialSize(initialSize);
        dataSource.setMaxActive(maxActive);
        dataSource.setMinIdle(minIdle);
        dataSource.setMaxWait(maxWait);
        dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        dataSource.setTestWhileIdle(testWhileIdle);
    }

    /**
     * 在实例化dynamicDataSource之前,需要保证master和demo数据源已注入IoC容器
     *
     * @Primary必不可少,多个数据源的前提下,引入MybatisPlusAutoConfiguration时需要有一个@Primary数据源才会后续注入SqlSessionFactory 且依赖注入DataSource时,返回DynamicDataSource
     */
    @Bean(name = "dynamicDataSource")
    @DependsOn({"masterDataSource", "slaveDataSource"})
    @Primary
    public DynamicDataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        dynamicDataSource.setWriteDataSource(masterDataSource());
        dynamicDataSource.setReadDataSource(slaveDataSource());
        return dynamicDataSource;
    }


    @Bean
    @ConditionalOnMissingBean({MybatisPlusInterceptor.class})
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 在这里可以扩展自己的MP拦截器组件
        interceptor.addInnerInterceptor(new WriteReadMybatisIntercepts());
        return interceptor;
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
  1. 动态数据源路由拦截器
@Data
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Object writeDataSource;
    private Object readDataSource;

    @Override
    public void afterPropertiesSet() {
        if (this.writeDataSource == null) {
            throw new IllegalArgumentException("Property 'writeDataSource' is required");
        }
        setDefaultTargetDataSource(writeDataSource);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceGlobalEnum.WRITE.name(), writeDataSource);
        if (readDataSource != null) {
            targetDataSources.put(DynamicDataSourceGlobalEnum.READ.name(), readDataSource);
        }
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        // 开启事务,用主数据源
		return DynamicDataSourceHolder.getDataSource() == DynamicDataSourceGlobalEnum.WRITE ? DynamicDataSourceGlobalEnum.WRITE.name() : DynamicDataSourceGlobalEnum.READ.name();
    }
}
  1. 线程变量
public interface DynamicDataSourceConstant {

    String THREAD_DS_KEY = "threadDsKey";
}

  1. 数据源key枚举
public enum DynamicDataSourceGlobalEnum {
    READ,
    WRITE;
}
  1. 数据源切换处理器
public class DynamicDataSourceHolder {

    private DynamicDataSourceHolder() {
    }

    public static void putDataSource(DynamicDataSourceGlobalEnum dataSource) {
        ThreadLocalUtil.put(DynamicDataSourceConstant.THREAD_DS_KEY, dataSource);
    }

    public static DynamicDataSourceGlobalEnum getDataSource() {
        Object tds = ThreadLocalUtil.get(DynamicDataSourceConstant.THREAD_DS_KEY);
        return tds == null ? DynamicDataSourceGlobalEnum.WRITE : (DynamicDataSourceGlobalEnum) tds;
    }

}
  1. MP的SQL拦截器
@Slf4j
public class WriteReadMybatisIntercepts implements InnerInterceptor {

    @Override
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobalEnum.READ);
        return true;
    }

    @Override
    public boolean willDoUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
        DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobalEnum.WRITE);
        return true;
    }

}

注意

  1. 如果service方法上加了事务,那么默认使用write数据源,同一事务中determineCurrentLookupKey只会触发一次,因为不能在同一个事务中切换数据源。如果没有事务,每次Dao都会触发determineCurrentLookupKey

你可能感兴趣的:(mybatis,spring,boot,java)