Springboot+MyBatis+MySQL实现读写分离

引言

读写分离就是对于一条SQL该选择哪一个数据库去执行,至于谁来做选择数据库这件事,主库一般用来执行“写”操作,从库用来执行“读”操作,从库可以有多个,主库从库之间的数据同步则是通过数据库间的异步线程进行通信。一般来说,读写分离有两种实现方式。第一种是依靠中间件MyCat,也就是说应用程序连接到中间件,中间件帮我们做SQL分离,去选择指定的数据源;第二种是应用程序自己去做分离。主要是利用Spring提供的路由数据源,以及AOP。

读写分离实现

读写分离需要的基础环境的的搭建:Linux下MySQL实现主从复制

1、pom中引入如下的依赖

		 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>   
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--SpringBoot集成Aop起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

2、application.yml中,配置各个数据库的参数

spring:
  datasource:
    master:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.152.173:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
      username: root
      password: 123456
    slave1:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.152.174:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
      username: root
      password: 123456
    slave2:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://192.168.152.175:3306/yiyun?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
      username: root
      password: 123456
  props:
    sql.show: true
  masterslave:
    # load-balance-algorithm-type是路由策略,round_robin表示轮询策略。
    load-balance-algorithm-type: round_robin
    # sharding.master-slave-rules是标明主库和从库,一定不要写错,否则写入数据到从库,就会导致无法同步。
  sharding:
    master-slave-rules:
      master:
        master-data-source-name: master
        slave-data-source-names: slave1,slave2

3、实现代码的编写

新建一个枚举类型,枚举值我们定义为 MASTER, SLAVE1,SLAVE2

/**
 * 主从类型
 *
 * @author: zhouzhou
 * @date:2022/2/11 11:24
 */
public enum DataSourceEnum {
    MASTER, SLAVE1 , SLAVE2;
}

动态切换数据源

/**
 * @author: zhouzhou
 * @date:2022/2/11 11:22
 *
 * 利用ThreadLocal封装的保存数据源上线的上下文context
 */
public class DataSourceContextHolder {

    /**
     * 确保创建的变量只能被同一个线程进行读和写的操作
     */
    private static final ThreadLocal<DataSourceEnum> contextHolder = new ThreadLocal<>();

    /**
     * 原子操作类,确保线程安全
     */
    private static final AtomicInteger counter = new AtomicInteger(-1);

    /**
     * 设置当前线程的枚举值
     * @param dbType
     */
    public static void set(DataSourceEnum dbType) {
        contextHolder.set(dbType);
    }

    /**
     * 获取当前线程的枚举值
     * @return
     */
    public static DataSourceEnum get() {
        return contextHolder.get();
    }

    /**
     * 切换到主库
     */
    public static void master() {
        set(DataSourceEnum.MASTER);
        System.out.println("切换到master");
    }

    /**
     * 切换到从库
     */
    public static void slave() {
        // 轮询 枚举值数量-1表示从库的数量
        int index = counter.getAndIncrement() % (DataSourceEnum.values().length-1);
        if (counter.get() > 9999) {
            counter.set(-1);
        }
        set(DataSourceEnum.values()[index+1]);
        System.out.println("切换到:"+DataSourceEnum.values()[index+1]);
    }

    /**
     * 移除当前线程的枚举值
     */
    public static void remove(){
        contextHolder.remove();
    }
}

然后新建一个叫DataSourceRouter的类,这个类需要继承AbstractRoutingDataSource这个抽象类,主要起路由到数据库的作用。在AbstractRoutingDataSource这个抽象类中,有一个叫targetDataSources的map,这里面可以存放我们的多个数据源。我们重写determineCurrentLookupKey方法,使用之前的DataSourceContextHolder去获取目前需要使用的是哪个库。

/**
 * @author: zhouzhou
 * @date:2022/2/11 11:21
 */
public class DataSourceRouter extends AbstractRoutingDataSource {

    /**
     * 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解
     * 应该给DataSourceContextHolder设值
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }

}

配置多数据源

新建一个名叫DataSourceConfig的类,在这个类中,我们初始化三个数据库的DataSource,然后再将这三个DataSource加载到targetDataSources中。

/**
 * 主从数据源配置
 *
 *  @author: zhouzhou
 *  @date:2022/2/11 11:30
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean("targetDataSources")
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                          @Qualifier("slave1DataSource") DataSource slave1DataSource,
                                          @Qualifier("slave2DataSource") DataSource slave2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.MASTER, masterDataSource);
        targetDataSources.put(DataSourceEnum.SLAVE1, slave1DataSource);
        targetDataSources.put(DataSourceEnum.SLAVE2, slave1DataSource);
        DataSourceRouter dataSourceRouter = new DataSourceRouter();
        dataSourceRouter.setDefaultTargetDataSource(masterDataSource);
        dataSourceRouter.setTargetDataSources(targetDataSources);
        return dataSourceRouter;
    }

}

mybatis配置

Spring容器中现在有3个数据源。我们需要创建SqlSessionFactoryConfig配置类,去初始化sqlSessionFactory,同时将刚才加载的数据源targetDataSource自动注入进来,然后指定好mapper的路径,这样,我们的数据库加载就初始化完成了。

/**
 * 由于Spring容器中现在有3个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
 *
 * @author: zhouzhou
 * @date:2022/2/11 15:01
 */
@Configuration
@MapperScan(basePackages = "com.zsn.yiyun.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
@EnableTransactionManagement
public class SqlSessionFactoryConfig {

    @Resource(name = "targetDataSources")
    private DataSource dataSource;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //设置mapper.xml文件所在位置
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
        Objects.requireNonNull(sqlSessionFactoryBean.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 事务管理器
     * @return
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager(){
        return new DataSourceTransactionManager(dataSource);
    }
}

AOP实现读写分离

AOP实现读写分离的方式就很多了,我们可以写注解@insert,@select,@update等等,通过注解去判断主从库,也可以直接通过方法名去判断。这里我们采用第二种方法去判断sql执行的数据库。

  • 读库:select*、get*等
  • 写库:insert*、add*、update*、edit*、delete*、remove*等

有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库

/**
 * 特殊情况下我们需要强制读主库,针对这种情况,我们定义一个注解,用该注解标注的就读主库
 *
 * @author: zhouzhou
 * @date:2022/2/11 14:34
 */
public @interface Master {
}
/**
 * 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
 * @author: zhouzhou
 * @date:2022/2/11 14:34
 */
@Aspect
@Component
public class DataSourceAop {
    @Pointcut("!@annotation(com.zsn.yiyun.aop.datasource.Master) " +
            "&& (execution(* com.zsn.yiyun.service.*.select*(..)) " +
            "|| execution(* com.zsn.yiyun.service..*.find*(..)))")
    public void readPointcut() {

    }
    @Pointcut("@annotation(com.zsn.yiyun.aop.datasource.Master) " +
            "|| execution(* com.zsn.yiyun.service..*.save*(..)) " +
            "|| execution(* com.zsn.yiyun.service..*.add*(..)) " +
            "|| execution(* com.zsn.yiyun.service..*.update*(..)) " +
            "|| execution(* com.zsn.yiyun.service..*.edit*(..)) " +
            "|| execution(* com.zsn.yiyun..*.delete*(..)) " +
            "|| execution(* com.zsn.yiyun..*.remove*(..))")
    public void writePointcut() {

    }

    @Before("readPointcut()")
    public void read() {
        DataSourceContextHolder.slave();
    }

    @Before("writePointcut()")
    public void write() {
        DataSourceContextHolder.master();
    }
}

至此,项目的基础搭建已基本完成,可实现读写分离。

参考:https://blog.csdn.net/mingwei_cheng/article/details/95232525

你可能感兴趣的:(数据库,Spring,boot,mysql,spring,boot,java)