• MySQL主从数据库(主读从写)


    MySQL主从数据库(主读从写)

    带薪学习:本文主要介绍主从数据库,在SpringBoot项目中我们需要连接多个数据源,多个数据库可能存在不同的服务上边,比如张三对MySQLA数据库只有读取数据的权限,对MySQLB数据库只有写数据的权限。这样的话一个项目中读取数据就要配置A的数据源,写入数据要配置B的数据源,这样就构成了多数据源切换问题。

    1. 多数据源的实现原理

    对于大多数的Java应用,都使用了spring架,spring-jdbc模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSoure,,然后在运行时来动态切换的访问数据库。这种方式对访问数据库的架构图如下所示:

    在这里插入图片描述

    应用直接操作的是AbstractRoutingDataSource的实现类,告诉AbstractRoutingDataSource访问哪个数据库,然后由AbstractRoutingDataSource从事先配置好的数据源(dbs1、dbs2)选择一个,来访问对应的数据库。

    在这里插入图片描述

    1. 配置多数据源和AbstractRoutingDataSource的自定义实现类:DynamicDataSource

    1.1 配置多数据源yml文件

    application.yaml

    
    spring:
      datasource:
        #主数据源
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://----:7788/guoguo?characterEncoding=utf-8&serviceTimezone=UTC
          username: root
          password: root
          type: com.alibaba.druid.pool.DruidDataSource
          initialSize: 10
          minIdle: 10
          maxActive: 30
        #从数据库
        slave:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://-----.mysql.rds.aliyuncs.com:3306/platform_crawler?characterEncoding=utf-8&serviceTimezone=UTC
          username: -----
          password: -----
          type: com.alibaba.druid.pool.DruidDataSource
          initialSize: 10
          minIdle: 10
          maxActive: 30
    
    
        masterslave:
          # 读写分离配置
          load-balance-algorithm-type: round_robin
          # 最终的数据源名称
          name: dataSource
          # 主库数据源名称
          master-data-source-name: master
          # 从库数据源名称列表,多个逗号分隔
          slave-data-source-names: slave
        props:
          # 开启SQL显示,默认false
          sql:
            show: true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    1.2 创建配置类

    DataSourceConfig.java

    package com.guo.mysql.config;
    
    import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Slf4j
    @Configuration
    public class DataSourceConfig {
    
        /**
         * 主数据源
         * @return DataSource
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            //底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
            return DruidDataSourceBuilder.create().build();
        }
    
        /**
         * 从 数据源
         * @return DataSource
         */
        @Bean  //
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            //底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
            return DruidDataSourceBuilder.create().build();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    1.3 动态切换数据源类继承AbstractRoutingDataSource 类

    DynamicDataSource.java

    package com.guo.mysql;
    
    import com.guo.mysql.enums.DataSourceEnum;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Primary;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    import org.springframework.stereotype.Component;
    
    import javax.sql.DataSource;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    @Primary   //将该Bean设置为主要注入Bean  implements DataSource, InitializingBean
    public class DynamicDataSource extends AbstractRoutingDataSource {
        //当前使用的数据源标识
        public static ThreadLocal<String> name = new ThreadLocal<>();
    
        //主写
        @Autowired
        DataSource masterDataSource;
    
        //从读
        @Autowired
        DataSource slaveDataSource;
    
        //返回当前数据源标识
        @Override
        public Object determineCurrentLookupKey() {
            return name.get();
        }
    
        /**
         * spring容器启动的时候被调用
         */
        @Override
        public void afterPropertiesSet() {
            //为targetDataSources初始化所有数据源
            Map<Object, Object> targetDataSources = new HashMap<>();
            targetDataSources.put(DataSourceEnum.主库.getName(), masterDataSource);
            targetDataSources.put(DataSourceEnum.从库.getName(), slaveDataSource);
            super.setTargetDataSources(targetDataSources);
            //为defaultTargetDataSource设置默认的数据源
            super.setDefaultTargetDataSource(slaveDataSource);   //从读
            super.afterPropertiesSet();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    1.4 测试类测试

    SpringbootSynchronizeMysqlApplicationTests.java
    测试类,根据指定数据库名称,切换数据源连接,进行操作不同的数据库

    package com.guo.mysql;
    
    import com.guo.mysql.enums.DataSourceEnum;
    import com.guo.mysql.service.UserOsskeyService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class SpringbootSynchronizeMysqlApplicationTests {
    
        @Test
        void contextLoads() {
        }
    
        @Autowired
        private UserOsskeyService userOsskeyService;
        @Test
        public void test(){
            DynamicDataSource.name.set(DataSourceEnum.从库.getName());
            System.out.println(userOsskeyService.count());
    
    
            DynamicDataSource.name.set(DataSourceEnum.主库.getName());
            System.out.println(userOsskeyService.count());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    输出:

    2022-11-15 11:03:11.165  INFO 11692 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
    569548
    2022-11-15 11:03:20.525  INFO 11692 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} inited
    1
    
    • 1
    • 2
    • 3
    • 4

    1.5 附枚举代码

    DataSourceEnum.java
    为了提高代码的可用性,在代码中切换数据源按照名称切换,这里把名称写在枚举中,后续如有修改直接在枚举类中修改就可以了,不需要去每个类中去修改,提高代码的可用性

    package com.guo.mysql.enums;
    
    import java.util.Collection;
    import java.util.EnumSet;
    import java.util.HashMap;
    import java.util.Map;
    
    public enum DataSourceEnum {
        主库("master", "w"),
        从库("slave", "r"),
        NULL("-1", "");
    
        private String name;
        private String exec;
    
        DataSourceEnum(String name, String exec) {
            this.name = name;
            this.exec = exec;
        }
    
        public String getName() {
            return name;
        }
    
        public String getExec() {
            return exec;
        }
    
        private static final Map<String, DataSourceEnum> codeMap = new HashMap<>((int) (values().length / .75f) + 1);
    
        static {
            for(DataSourceEnum item : values()) {
                codeMap.put(item.name, item);
            }
        }
    
        public static DataSourceEnum fromCode(String name) {
            DataSourceEnum item = codeMap.get(name);
            return item == null ? NULL : item;
        }
    
        public static Collection<DataSourceEnum> all() {
            EnumSet<DataSourceEnum> enumSet = EnumSet.allOf(DataSourceEnum.class);
            enumSet.remove(NULL);
            return enumSet;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    1.6 总结

    以上代码虽然可以实现基本数据源的切换,还存在一些问题

    1. 数据源表示设置代码耦合性过高,对后期维护造成很大的麻烦

    2. 多数据源切换方式(优化)

    1. MyBatis插件(读写分离)
    2. AOP方式切换数据源(业务复杂数据量大)

    2.1 MyBatis插件(读写分离)

    为了解决数据库的读性能瓶颈(读比写性能更高,写锁会影响读阻塞,从而影响读的性能)
    很对数据拥有主从架构,也就是,一台数据库服务器,是对外提供增删改业务的生产服务器;另外一(多)台数据库服务器,主要进行操作。
    可以通过中间件(ShardingSphere, mycat,mysql-proxy,TDDL …)

    这里的架构上类似。不同的是,在读写分离中,主库和从库的数据库是一致的(不考虑主从延迟)。数据更新操作(insert,update,delete)都是在主库上进行,主库将数据变更信息同步给从库。在查询时,可以在从库上进行,从而分担主库的压力
    在这里插入图片描述

    2.1.1 创建插件实现数据源的动态切换

    DynamicDataSourcePlugin.java

    package com.guo.mysql.plugin;
    
    import com.guo.mysql.DynamicDataSource;
    import com.guo.mysql.enums.DataSourceEnum;
    import org.apache.ibatis.executor.Executor;
    import org.apache.ibatis.mapping.MappedStatement;
    import org.apache.ibatis.mapping.SqlCommandType;
    import org.apache.ibatis.plugin.Interceptor;
    import org.apache.ibatis.plugin.Intercepts;
    import org.apache.ibatis.plugin.Invocation;
    import org.apache.ibatis.plugin.Signature;
    import org.apache.ibatis.session.ResultHandler;
    import org.apache.ibatis.session.RowBounds;
    
    import java.util.Properties;
    //Mybatis拦截器注解
    //@Intercepts:标识该类是一个拦截器
    //@Signature:拦截器相关属性设置
    @Intercepts({
            @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
            @Signature(type = Executor.class,method = "query", args={MappedStatement.class, Object.class, RowBounds.class,
                    ResultHandler.class})
    })
    public class DynamicDataSourcePlugin implements Interceptor {
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //拿到当前方法(update, query)所有参数
            Object[] objects = invocation.getArgs();
            //MappedStatement 封装SQL
            MappedStatement ms = (MappedStatement) objects[0];
            //读方法
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
                DynamicDataSource.name.set(DataSourceEnum.从库.getName());
            }else {
                //写操作
                DynamicDataSource.name.set(DataSourceEnum.主库.getName());
            }
            // 修改当前线程要选择的数据源的Key
            return invocation.proceed();
        }
        @Override
        public Object plugin(Object target) {
            return Interceptor.super.plugin(target);
        }
    
        @Override
        public void setProperties(Properties properties) {
            Interceptor.super.setProperties(properties);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    2.1.2 在配置类中添加数据源动态切换的插件bean对象
    package com.guo.mysql.config;
    
    import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
    import com.guo.mysql.plugin.DynamicDataSourcePlugin;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.plugin.Interceptor;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import javax.sql.DataSource;
    
    @Slf4j
    @Configuration
    public class DataSourceConfig {
    
        /**
         * 主数据源
         * @return DataSource
         */
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.master")
        public DataSource masterDataSource() {
            //底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
            return DruidDataSourceBuilder.create().build();
        }
    
        /**
         * 从 数据源
         * @return DataSource
         */
        @Bean  //
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        public DataSource slaveDataSource() {
            //底层自动拿到Spring.datasource中的配置,创建一个DruidDatasource
            return DruidDataSourceBuilder.create().build();
        }
    
    
        //mybatisplus 注入切换数据源的插件bean对象
        @Bean
        public Interceptor dynamicDataSourcePlugin(){
            return new DynamicDataSourcePlugin();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    核心代码解读

    //拿到当前方法(update, query)所有参数
    Object[] objects = invocation.getArgs();
    //MappedStatement 封装SQL
    MappedStatement ms = (MappedStatement) objects[0];
     //读方法
    if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){
        DynamicDataSource.name.set(DataSourceEnum.从库.getName());
    }else {
        //写操作
        DynamicDataSource.name.set(DataSourceEnum.主库.getName());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    通过学习spring底层源码了解到,当我们操作查询SELECT操作的时候调用从库(从库读取数据),操作添加更新删除操作的时候 INSERT, UPDATE, DELETE,调用我们的主库(主库用来增删改操作);
    通过上述判断修改当前线程要选择的数据源的Key

    2.2 AOP方式切换数据源(业务复杂数据量大)

    数据分布在不同的数据库中,数据库拆了,应用没有拆,一个公司多个子项目,各用各分数据库,涉及数据共享…

    不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源
    AOP+自定义注解
    在这里插入图片描述

    2.2.1 使用AOP首先需要程序的核心启动类上添加注解

    @EnableAspectJAutoProxy //启动AOP

    package com.guo.mysql;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @SpringBootApplication
    @EnableAspectJAutoProxy  //启动AOP
    public class SpringbootSynchronizeMysqlApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootSynchronizeMysqlApplication.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    2.2.2 自定义注解

    WR.java

    package com.guo.mysql.annotation;
    
    import com.guo.mysql.enums.DataSourceEnum;
    
    import javax.xml.bind.Element;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 自定义注解
     * 允许在方法和类型上使用该注解
     */
    @Target({ElementType.METHOD,ElementType.TYPE})
    //保留方式   SOURCE不会编译在class文件中   CLASS:会编译不会被JVM加载,通过反射获取不到   RUNTIME:可以通过反射调用
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WR {
        String value() default "slave";   //默认是从库(大多数场景都是在读取数据)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    2.2.3 切面类

    DynamicDataSourceAspect .java

    package com.guo.mysql.aspect;
    
    import com.guo.mysql.DynamicDataSource;
    import com.guo.mysql.annotation.WR;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class DynamicDataSourceAspect {
        //前置通知   within指定包下的所有类
        @Before("within(com.guo.mysql.service.impl.*) && @annotation(wr)")
        public void before(JoinPoint point, WR wr){
            String name = wr.value();
            DynamicDataSource.name.set(name);
            System.out.println(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    2.2.4 使用注解

    上边切面类中自定义切面的时指定的包为接口实现类中,所以这里需要在指定的包下使用注解,实现数据源的动态切换

    package com.guo.mysql.service.impl;
    
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.guo.mysql.annotation.WR;
    import com.guo.mysql.entity.UserOsskey;
    import com.guo.mysql.mapper.UserOsskeyMapper;
    import com.guo.mysql.service.UserOsskeyService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    @Service
    public class UserOsskeyServiceImpl extends ServiceImpl<UserOsskeyMapper, UserOsskey> implements UserOsskeyService {
        @Resource
        private UserOsskeyMapper userOsskeyMapper;
    
        // 从库查询
        @Override
        @WR("slave")
        public List<UserOsskey> queryList(QueryWrapper<UserOsskey> queryWrapper) {
            return userOsskeyMapper.selectList(queryWrapper);
        }
        //主库插入
        @Override
        @WR("master")
        public int saveData(UserOsskey userOsskey) {
            return userOsskeyMapper.insert(userOsskey);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
  • 相关阅读:
    交换机和路由器技术-15-链路聚合
    选出表中的中位数记录[构造左右边界 || 问题转换]
    MP3算法及代码例程
    redis高级案列case
    koa实战 (一):项目搭建
    【pytorch笔记】第二篇 Pytorch加载数据
    Gitlab CI/CD 入门教程
    最新 Hugging Face 强化学习课程(中文版)来啦!
    Hbase API
    客户端和服务器不支持常用的SSL协议版本或密码套件
  • 原文地址:https://blog.csdn.net/qq_45896330/article/details/127857611