• Spring-Boot 多数据源配置+动态数据源切换+多数据源事物配置实现主从数据库存储分离


    一、基础介绍

      多数据源字面意思,比如说二个数据库,甚至不同类型的数据库。在用SpringBoot开发项目时,随着业务量的扩大,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。

    二、项目目录截图

     三、多数据源SQL结构设计如下(简单的主从关系):

     PS:创建两个库用于搭建项目中主从使用不同的数据库,表可以随意定义。

     四、配置编码

    1.数据源自定义注解,DataSource.java

    /**
     * 数据源自定义注解
     */
    
    @Target({ ElementType.METHOD, ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface DataSource {
        
        DataSourcesType name() default DataSourcesType.MASTER;
    
    }

    2.数据源类型枚举类定义,DataSourcesType.java 

    /**
     * 数据源类型
     */
    public enum  DataSourcesType {
        /**
         * 主库
         */
        MASTER,
    
        /**
         * 从库
         */
        SLAVE
    
    }

     3.多数据源application.yml配置文件配置

    # 数据源配置
    spring:
        datasource:
          type: com.alibaba.druid.pool.DruidDataSource
          driverClassName: com.mysql.cj.jdbc.Driver
          druid:
              master:
                  url: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                  username: root
                  password: 123456
              slave:
                  enable: true
                  url: jdbc:mysql://127.0.0.1:3306/slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                  username: root
                  password: 123456
              # 初始连接数
              initialSize: 5
              # 最小连接池数量
              minIdle: 10
              # 最大连接池数量
              maxActive: 20
              # 配置获取连接等待超时的时间
              maxWait: 60000
              # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
              timeBetweenEvictionRunsMillis: 60000
              # 配置一个连接在池中最小生存的时间,单位是毫秒
              minEvictableIdleTimeMillis: 300000
              # 配置一个连接在池中最大生存的时间,单位是毫秒
              maxEvictableIdleTimeMillis: 900000
              validationQuery: SELECT 1 FROM DUAL
              testWhileIdle: true
              testOnBorrow: false
              testOnReturn: false
              # 打开PSCache,并且指定每个连接上PSCache的大小
              poolPreparedStatements: true
              maxPoolPreparedStatementPerConnectionSize: 20
              # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙,此处是filter修改的地方
              filters:
                commons-log.connection-logger-name: stat,wall,log4j
              # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
              connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
              # 合并多个DruidDataSource的监控数据
              useGlobalDataSourceStat: true
              # 配置 DruidStatFilter
              web-stat-filter:
                enabled: true
                url-pattern: /*
                exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
              stat-view-servlet:
                enabled: true
                url-pattern: /druid/*
                # IP 白名单,没有配置或者为空,则允许所有访问
                allow: 127.0.0.1
                # IP 黑名单,若白名单也存在,则优先使用
                deny: 192.168.31.253
                # 禁用 HTML 中 Reset All 按钮
                reset-enable: false
                # 登录用户名/密码
                login-username: root
                login-password: 123
                # 慢SQL记录
              filter:
                  stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                  wall:
                    config:
                      multi-statement-allow: true

    4.数据源配置文件属性定义,DataSourceProperties.java

    /**
     * 数据源配置文件
     */
    @Setter
    @Configuration
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public class DataSourceProperties {
    
        private int initialSize;
    
        private int minIdle;
    
        private int maxActive;
    
        private int maxWait;
    
        private int timeBetweenEvictionRunsMillis;
    
        private int minEvictableIdleTimeMillis;
    
        private int maxEvictableIdleTimeMillis;
    
        private String validationQuery;
    
        private boolean testWhileIdle;
    
        private boolean testOnBorrow;
    
        private boolean testOnReturn;
    
        public DruidDataSource setDataSource(DruidDataSource datasource) {
    
            datasource.setInitialSize(initialSize);
            /** 配置初始化大小、最小、最大 */
            datasource.setInitialSize(initialSize);
            datasource.setMaxActive(maxActive);
            datasource.setMinIdle(minIdle);
            /** 配置获取连接等待超时的时间 */
            datasource.setMaxWait(maxWait);
            /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
            datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
            datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
            datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
            /**
             * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
             */
            datasource.setValidationQuery(validationQuery);
            /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
            datasource.setTestWhileIdle(testWhileIdle);
            /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
            datasource.setTestOnBorrow(testOnBorrow);
            /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
            datasource.setTestOnReturn(testOnReturn);
            return datasource;
        }

    5.多数据源切换处理,DynamicDataSourceContextHolder.java

    /**
     * 数据源切换处理
     */
    public class DynamicDataSourceContextHolder {
    
        public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
    
        /**
         *此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
         */
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
        /**
         * 设置当前线程的数据源变量
         */
        public static void setDataSourceType(String dataSourceType) {
            log.info("已切换到{}数据源", dataSourceType);
            contextHolder.set(dataSourceType);
        }
    
        /**
         * 获取当前线程的数据源变量
         */
        public static String getDataSourceType() {
            return contextHolder.get();
        }
    
        /**
         * 删除与当前线程绑定的数据源变量
         */
        public static void removeDataSourceType() {
            contextHolder.remove();
        }
    
    
    }

     6.获取数据源(依赖于 spring) 定义一个类继承AbstractRoutingDataSource实现determineCurrentLookupKey方法,该方法可以实现数据库的动态切换,DynamicDataSource.java 

    /**
     * 获取数据源(依赖于 spring)  定义一个类继承AbstractRoutingDataSource实现determineCurrentLookupKey方法,该方法可以实现数据库的动态切换
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    
        public static  DynamicDataSource build() {
            return new DynamicDataSource();
        }
    
        /**
         * 获取与数据源相关的key
         * 此key是Map<String,DataSource> resolvedDataSources 中与数据源绑定的key值
         * 在通过determineTargetDataSource获取目标数据源时使用
         */
        @Override
        protected Object determineCurrentLookupKey() {
            return DynamicDataSourceContextHolder.getDataSourceType();
        }
    
    }

    7.数据源核心配置类,DataSourceConfiguration.java 

    /**
     * 数据源配置类
     */
    @Configuration
    public class DataSourceConfiguration {
    
        /**
         * 主库
         */
        @Bean
        @ConfigurationProperties("spring.datasource.druid.master")
        public DataSource masterDataSource(DataSourceProperties dataSourceProperties) {
            return dataSourceProperties.setDataSource(DruidDataSourceBuilder.create().build());
        }
    
    
        /**
         * 从库
         */
        @Bean
        @ConditionalOnProperty( prefix = "spring.datasource.druid.slave", name = "enable", havingValue = "true")//是否开启数据源开关---若不开启 默认适用默认数据源
        @ConfigurationProperties("spring.datasource.druid.slave")
        public DataSource slaveDataSource(DataSourceProperties dataSourceProperties) {
            return dataSourceProperties.setDataSource(DruidDataSourceBuilder.create().build());
        }
    
        /**
         * 设置数据源
         */
        @Bean(name = "dynamicDataSource")
        @Primary
        public DynamicDataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
            Map<Object, Object> targetDataSources = new HashMap<>();
            DynamicDataSource dynamicDataSource = DynamicDataSource.build();
            targetDataSources.put(DataSourcesType.MASTER.name(), masterDataSource);
            targetDataSources.put(DataSourcesType.SLAVE.name(), slaveDataSource);
            //默认数据源配置 DefaultTargetDataSource
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
            //额外数据源配置 TargetDataSources
            dynamicDataSource.setTargetDataSources(targetDataSources);
            dynamicDataSource.afterPropertiesSet();
            return dynamicDataSource;
        }
    
    }

    8.多数据源切面配置类,用于获取注解上的注解,进行动态切换数据源DynamicDataSourceAspect.java

    @Aspect
    @Component
    @Order(-1) // 保证该AOP在@Transactional之前执行
    public class DynamicDataSourceAspect {
    
        protected Logger logger = LoggerFactory.getLogger(getClass());
    
    
        @Pointcut("@annotation(com.fuzongle.tankboot.common.annotation.DataSource)"
                + "|| @within(com.fuzongle.tankboot.common.annotation.DataSource)")
        public void dsPointCut()  {
        }
    
        @Around("dsPointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            Method targetMethod = this.getTargetMethod(point);
            DataSource dataSource = targetMethod.getAnnotation(DataSource.class);//获取要切换的数据源
            if (dataSource != null)  {
                DynamicDataSourceContextHolder.setDataSourceType(dataSource.name().name());
            }
            try {
                return point.proceed();
            }
            finally  {
                // 销毁数据源 在执行方法之后
                DynamicDataSourceContextHolder.removeDataSourceType();
            }
        }
    
        /**
         * 获取目标方法
         */
        private Method getTargetMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
            Signature signature = pjp.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method agentMethod = methodSignature.getMethod();
            return pjp.getTarget().getClass().getMethod(agentMethod.getName(), agentMethod.getParameterTypes());
        }
    }
    9.编写业务逻辑,切换从库查询数据。

     10.编写测试方法,调用查询业务,查看是否切换数据源是否生效。

    PS:这种多数据源的动态切换确实可以解决数据的主从分库操作,但是却有一个致命的BUG,那就是事务不但失效而且无法实现

    一致性,因为涉及到跨库,因此我们必须另想办法来实现事务的ACID原则

            以上配置所有源码地址:https://gitee.com/fuzongle/java-bucket

    注意:

    1.如果有任何不懂的地方可以关注公众号就可以加我微信,随时欢迎互相帮助。

    2.技术交流群QQ:422167709。

    3.如果希望学习更多,希望微信扫码,长按扫码,帮忙关注一下,举手之劳,当您无助的时候真的能帮你。非常感谢您关注公众号 "编程小乐"。

  • 相关阅读:
    鼠标不灵了,还好只是线的问题。自己DIY修下了
    [摘]编译MPlayer
    TPLINK路由器 硬重启方法
    Visual C++线程同步技术剖析 (转载)
    CListCtrl一行显示多个图标问题
    一位软件工程师的6年总结
    CCIE红头发讲解CCNA、CCNP视频教程
    图片链
    [摘]如何级联两个TPLINK路由器
    [摘]测试一下你对IP地址的掌握水平
  • 原文地址:https://www.cnblogs.com/fuzongle/p/13335304.html
Copyright © 2020-2023  润新知