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

MySQL多数据源

  • MySQL主从数据库(主读从写)
    • 1. 多数据源的实现原理
      • 1.1 配置多数据源yml文件
      • 1.2 创建配置类
      • 1.3 动态切换数据源类继承AbstractRoutingDataSource 类
      • 1.4 测试类测试
      • 1.5 附枚举代码
      • 1.6 总结
    • 2. 多数据源切换方式(优化)
      • 2.1 MyBatis插件(读写分离)
        • 2.1.1 创建插件实现数据源的动态切换
        • 2.1.2 在配置类中添加数据源动态切换的插件bean对象
      • 2.2 AOP方式切换数据源(业务复杂数据量大)
        • 2.2.1 使用AOP首先需要程序的核心启动类上添加注解
        • 2.2.2 自定义注解
        • 2.2.3 切面类
        • 2.2.4 使用注解

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

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

1. 多数据源的实现原理

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

MySQL主从数据库(主读从写)_第1张图片

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

MySQL主从数据库(主读从写)_第2张图片

  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 创建配置类

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.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.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());
    }
}

输出:

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.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.6 总结

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

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

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

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

2.1 MyBatis插件(读写分离)

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

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

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);
    }
}

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();
    }
}

核心代码解读

//拿到当前方法(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());
}

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

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

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

不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源
AOP+自定义注解
MySQL主从数据库(主读从写)_第4张图片

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);
    }
}

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";   //默认是从库(大多数场景都是在读取数据)
}

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);
    }
}

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);
    }
}

你可能感兴趣的:(MyBatis-Plus,MySQL,Spring,Boot,数据库,mysql,java)