SpringBoot+MybatisPlus配置读写分离

1.概述

继承AbstractRoutingDataSource接口实现读写分离配置。使用的主要技术如下:

  • SpringBoot 2.1.12.RELEASE
  • MybatisPlus
  • alibaba.druid数据库连接池
  • mysql数据库
  • SpringAop

2.配置文件

mybatis-plus:
  # 如果是放在src/main/java目录下 classpath:/com/yourpackage/*/mapper/*Mapper.xml
  # 如果是放在resource目录 classpath:/mapper/*Mapper.xml
  mapper-locations: classpath:com/bbdog/dao/xml/*Mapper.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.bbdog.dao.model
  global-config:
    #主键类型  0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
    id-type: 0
    #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
    field-strategy: 1
    #刷新mapper 调试神器
    refresh-mapper: true
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    #配置JdbcTypeForNull
    jdbc-type-for-null: 'null'

spring:
  datasource:
    master:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://${spring.virtualIp}:3306/bbdog
      username: master
      password: ******
      #----数据库连接池配置----------------------
      # 下面为连接池的补充设置,应用到上面所有数据源中
      # 初始化大小,最小,最大
      initialSize: 5
      minIdle: 1
      maxActive: 50
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: false
      #maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties:
          druid:
            stat:
              mergeSql: true
              slowSqlMillis: 5000
      # 合并多个DruidDataSource的监控数据
      #useGlobalDataSourceStat: true
    slave:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://${spring.virtualIp}:3306/bbdog
      username: slave
      password: ******
      #----数据库连接池配置----------------------
      # 下面为连接池的补充设置,应用到上面所有数据源中
      # 初始化大小,最小,最大
      initialSize: 5
      minIdle: 1
      maxActive: 50
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: false
      #maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties:
          druid:
            stat:
              mergeSql: true
              slowSqlMillis: 5000
      # 合并多个DruidDataSource的监控数据
      #useGlobalDataSourceStat: true

3.SpringBoot启动类设置

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) // 设置动态数据源需要,禁用数据源自动配置
@EnableTransactionManagement//开启springBoot事务
@MapperScan("com.bbdog.dao.mapper*")
@EnableCaching//开启基于注解的缓存
public class WebApplication {

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

}

4.创建数据源类型

public enum SourceName {
    read("read"), write("write");

    private String value;

    SourceName(String value) {
        this.value = value;
    }

    public String value() {
        return this.value;
    }
}

5.构建切换数据源类

public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 read 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return SourceName.read.value();
        }
    };


    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 切换数据源
     *
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 获取数据源
     *
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置数据源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判断是否包含数据源
     *
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源keys
     *
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
}

6.继承AbstractRoutingDataSource接口实现动态数据源

public class AutoChooseDataSource extends AbstractRoutingDataSource {

    /**
     * 如果不希望数据源在启动配置时就加载好,可以定制这个方法,从任何你希望的地方读取并返回数据源
     * 比如从数据库、文件、外部接口等读取数据源信息,并最终返回一个DataSource实现类对象即可
     */
    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     * 如果希望所有数据源在启动配置时就加载好,这里通过设置数据源Key值来切换数据,定制这个方法
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 设置默认数据源
     *
     * @param defaultDataSource
     */
    @Override
    public void setDefaultTargetDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
     * 设置数据源
     *
     * @param dataSources
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }
}

7.数据源配置类设置

参照自动配置类MybatisPlusAutoConfiguration.java中的SqlSessionFactory配置来为添加自己的动态数据源

@SuppressWarnings("ConstantConditions")
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean(DataSource.class)//容器中有DataSource类就可以调用该配置类的方法了
@EnableConfigurationProperties(MybatisPlusProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisPlusAutoConfiguration {   

    ...
        
    /*
    将自己配置的动态数据源放入容器中,容器会自动注入到该方法的入参。
    由于容器中有多个DataSource类,所以要将自己的动态数据源设置为默认@Primary
    */
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        ....
        return factory.getObject();
    }
}

数据源配置类内容:

@Configuration
public class DruidConfiguration {

    /**
     * 动态数据源配置**********************************↓↓↓↓↓↓↓↓↓↓↓↓↓↓
     ***************************/

    @Bean(name = "write", destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource master() {
        return druidDataSource();
    }

    @Bean(name = "read", destroyMethod = "close", initMethod = "init")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slave() {
        return druidDataSource();
    }

    @Bean("dataSource")
    @Primary//自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常
    public DataSource autoChooseDataSource() {
        AutoChooseDataSource autoChooseDataSource = new AutoChooseDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put(SourceName.write.value(), master());
        dataSourceMap.put(SourceName.read.value(), slave());
        // 将 read 数据源作为默认指定的数据源
        autoChooseDataSource.setDefaultTargetDataSource(slave());
        // 将 read 和 write 数据源作为指定的数据源
        autoChooseDataSource.setTargetDataSources(dataSourceMap);
        return autoChooseDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        // 配置事务管理, 使用事务时在方法头部添加@Transactional注解即可
        return new DataSourceTransactionManager(autoChooseDataSource());
    }

    /**
     * 动态数据源配置**********************************↑↑↑↑↑↑↑↑↑↑↑↑↑↑
     ***************************/

    public DataSource druidDataSource() {
        return new DruidDataSource();
    }
}

8.创建数据源切换注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    /**
     * 数据源key值
     * @return
     */
    SourceName value();

}

9.创建数据源切换切面

@Aspect
@Order(-1)  // 该切面应当先于 @Transactional 执行
@Component
public class DynamicDataSourceAspect {
    private static Logger _log = LoggerFactory.getLogger(DynamicDataSourceAspect.class);

    /**
     * 切换数据源
     *
     * @param point
     * @param dataSource
     */
    @Before("@annotation(dataSource))")
    public void switchDataSource(JoinPoint point, DataSource dataSource) {
        if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value().name())) {
            _log.error("DataSource [{}] 不存在,使用默认 DataSource [{}] ",
                    dataSource.value(),
                    DynamicDataSourceContextHolder.getDataSourceKey());
        } else {
            // 切换数据源
            DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value().name());
            _log.debug("切换 DataSource 至 [{}] ,引起切换方法是 [{}]",
                    DynamicDataSourceContextHolder.getDataSourceKey(),
                    point.getSignature());
        }
    }

    /**
     * 重置数据源
     *
     * @param point
     * @param dataSource
     */
    @After("@annotation(dataSource))")
    public void restoreDataSource(JoinPoint point, DataSource dataSource) {
        // 将数据源置为默认数据源
        DynamicDataSourceContextHolder.clearDataSourceKey();
        _log.debug("重置 DataSource 至 [{}] ,引起重置的方法是 [{}]",
                DynamicDataSourceContextHolder.getDataSourceKey(),
                point.getSignature());
    }
}

10.示例

    /**
     * 删除角色
     *
     * @param role
     * @return
     */
    @Override
    @DataSource(SourceName.write)
    @Transactional
    public R deleteRoleById(Role role) {
        role.setUpdateTime(Utils.getCurrentFormatDateStr());
        role.setValid("0");
        Integer delete = baseMapper.updateById(role);
        this.clearPermissionsInRoleId(role.getRoleId());
        return new R(1 == delete);
    }
posted @   howard4  阅读(6599)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
点击右上角即可分享
微信分享提示