SpringBoot读写分离

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
简介: SpringBoot读写分离

前提


  • 2台mysql

     ①可以开两个docker   ②也可以在一个MySQL服务器中用2个数据库


 docker run -d -p 3307:3306 --name mysql01 -e MYSQL_ROOT_PASSWORD=123456  docker.io/mysql
 docker run -d -p 3308:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456  docker.io/mysql


  • SpringBoot

代码


代码下载:  Demooo/springboot-readwrite-separation-deno at master · cbeann/Demooo · GitHub


目录结构


2.png


maven依赖


    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>


配置文件application.yml


spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://202.204.124.110:3306/readwritedemo
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave:
      jdbc-url: jdbc:mysql://202.204.124.110:3306/readwritedemo
      username: root   # 只读账户
      password: root
      driver-class-name: com.mysql.jdbc.Driver


数据源配置


DataSourceConfig


此处设计了两个数据源,一个是master,一个是slave


package com.example.config;
/**
 * @author chaird
 * @create 2020-12-30 21:27
 */
import com.example.enums.DBTypeEnum;
import com.example.bean.MyRoutingDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
 * 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》 79. Data Access 79.1 Configure a Custom DataSource 79.2
 * Configure Two DataSources
 */
@Configuration
public class DataSourceConfig {
  @Bean
  @ConfigurationProperties("spring.datasource.master")
  public DataSource masterDataSource() {
    return DataSourceBuilder.create().build();
  }
  @Bean
  @ConfigurationProperties("spring.datasource.slave")
  public DataSource slaveDataSource() {
    return DataSourceBuilder.create().build();
  }
  @Bean
  public DataSource myRoutingDataSource(
      @Qualifier("masterDataSource") DataSource masterDataSource,
      @Qualifier("slaveDataSource") DataSource slave1DataSource) {
    Map<Object, Object> targetDataSources = new HashMap<>(2);
    targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
    targetDataSources.put(DBTypeEnum.SLAVE, slave1DataSource);
    MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
    myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
    myRoutingDataSource.setTargetDataSources(targetDataSources);
    return myRoutingDataSource;
  }
}


MyBatisConfig  


MyBatis配置xml位置,事务管理器等


package com.example.config;
/**
 * @author chaird
 * @create 2020-12-30 21:27
 */
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
  @Resource(name = "myRoutingDataSource")
  private DataSource myRoutingDataSource;
  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
    sqlSessionFactoryBean.setMapperLocations(
        new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
    return sqlSessionFactoryBean.getObject();
  }
  @Bean
  public PlatformTransactionManager platformTransactionManager() {
    return new DataSourceTransactionManager(myRoutingDataSource);
  }
}


DBTypeEnum 枚举类


package com.example.enums;
/**
 * @author chaird
 * @create 2020-12-30 21:28
 */
public enum DBTypeEnum {
  MASTER,
  SLAVE;
}


MyRoutingDataSource  


AbstractRoutingDataSource 一个神奇的接口,自己在determineCurrentLookupKey方法debug就清楚了。


package com.example.bean;
/**
 * @author chaird
 * @create 2020-12-30 21:37
 */
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
public class MyRoutingDataSource extends AbstractRoutingDataSource {
  @Nullable
  @Override
  protected Object determineCurrentLookupKey() {
    return DBContextHolder.get();
  }
}


AOP设置


DataSourceAop


设置切面和切面方法


package com.example.aop;
/**
 * @author chaird
 * @create 2020-12-30 21:30
 */
import com.example.bean.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/** 切面表达式和方法 */
@Aspect
@Component
public class DataSourceAop {
  /** 读切面(条件1&&条件2)
   * 条件1:没有Master注解 条件2:com.example.service包下 任意类 的select* 或者get*方法
   * */
  @Pointcut(
      "!@annotation(com.example.annotation.Master) "
          + "&& (execution(* com.example.service..*.select*(..)) "
          + "|| execution(* com.example.service..*.get*(..)))")
  public void readPointcut() {}
    /** 写切面
     * 类似上面,不做解释
     * */
  @Pointcut(
      "@annotation(com.example.annotation.Master) "
          + "|| execution(* com.example.service..*.insert*(..)) "
          + "|| execution(* com.example.service..*.add*(..)) "
          + "|| execution(* com.example.service..*.update*(..)) "
          + "|| execution(* com.example.service..*.edit*(..)) "
          + "|| execution(* com.example.service..*.delete*(..)) "
          + "|| execution(* com.example.service..*.remove*(..))")
  public void writePointcut() {}
    /**
     * Before方法,设置ThreadLocal里的一个变量为slave
     */
  @Before("readPointcut()")
  public void read() {
    DBContextHolder.slave();
  }
    /**
     * Before方法,设置ThreadLocal里的一个变量为master
     */
  @Before("writePointcut()")
  public void write() {
    DBContextHolder.master();
  }
  /** 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库 */
  //    @Before("execution(* com.cjs.example.service.impl.*.*(..))")
  //    public void before(JoinPoint jp) {
  //        String methodName = jp.getSignature().getName();
  //
  //        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
  //            DBContextHolder.slave();
  //        }else {
  //            DBContextHolder.master();
  //        }
  //    }
}


Master


自定义注解,有该注解的的service方法出现主库


package com.example.annotation;
/**
 * @author chaird
 * @create 2020-12-30 21:35
 * 加此注解的的方法查询主库
 */
public @interface Master {}


DBContextHolder


给threadLocal设置参数


package com.example.bean;
/**
 * @author chaird
 * @create 2020-12-30 21:30
 */
import com.example.enums.DBTypeEnum;
import java.util.concurrent.atomic.AtomicInteger;
public class DBContextHolder {
  private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
  private static final AtomicInteger counter = new AtomicInteger(-1);
  public static void set(DBTypeEnum dbType) {
    contextHolder.set(dbType);
  }
  public static DBTypeEnum get() {
    return contextHolder.get();
  }
  public static void master() {
    set(DBTypeEnum.MASTER);
    System.out.println("切换到master");
  }
  public static void slave() {
    //  轮询
    set(DBTypeEnum.SLAVE);
    System.out.println("切换到slave2");
  }
}


entity层


package com.example.entity;
import java.io.Serializable;
import lombok.Data;
/**
 * student
 * @author 
 */
@Data
public class Student implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private static final long serialVersionUID = 1L;
}


mapper、xml(省略)


service层


package com.example.service;
import com.example.annotation.Master;
import com.example.dao.StudentDao;
import com.example.entity.Student;
import com.example.entity.StudentExample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
 * @author chaird
 * @create 2020-12-30 21:38
 */
@Service
public class StudentService {
  @Autowired private StudentDao studentDao;
  public List<Student> getAllSlave() {
    return studentDao.selectByExample(new StudentExample());
  }
  @Master
  public List<Student> getAllMaster() {
    return studentDao.selectByExample(new StudentExample());
  }
  public void add(Student student) {
    studentDao.insert(student);
  }
}


接口层


package com.example.controller;
import com.example.entity.Student;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author chaird
 * @create 2020-12-30 21:40
 */
@RestController
public class StudentController {
  @Autowired private StudentService studentService;
  @GetMapping("/getAllMaster")
  public Object getAllMaster() {
    return studentService.getAllMaster();
  }
  @GetMapping("/getAllSlave")
  public Object getAllSlave() {
    return studentService.getAllSlave();
  }
  @PostMapping("/save")
  public Object save(@RequestBody Student student) {
    studentService.add(student);
    return "success";
  }
}


启动类


package com.example;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.dao")//扫描DAO包
public class ReadwriteSepApp {
  public static void main(String[] args) {
    SpringApplication.run(ReadwriteSepApp.class, args);
  }
}


测试


查询走写库:http://localhost:8080/getAllMaster


查询走读库:http://localhost:8080/getAllSlave


增加走写库:http://localhost:8080/save


原理


注意:该原理流程图是根据我上面的代码写的


1)创建两个数据源,并放在AbstractRoutingDataSource里的一个属性map中,其中(master->dataSource1,slave->dataSource2)


2)  先执行方法的AOP的Before方法,根据方法名称getAllSlave()方法在ThreadLocal中设置为 slave


3)  执行查询的时候需要获取dataSource,获取dataSource时AbstractRoutingDataSource.determineCurrentLookupKey()方法,拿到了步骤2)中设置slave,


4)从步骤3)中拿到了slave属性,然后在步骤1)放入到的AbstractRoutingDataSource里的map里拿到dataSource


5)从而实现了变换dataSource


3.jpg


总结


1)ThreadLocal竟然可以用在此处,惊呆了,我的小伙伴

2)AOP也是秀

3)XXXHolder不知道出自哪,但是我看了几篇博客,都是这种编码风格,例如DBContextHolder

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
4月前
|
Java 关系型数据库 MySQL
②⑩② 【读写分离】Sharding - JDBC 实现 MySQL读写分离[SpringBoot框架]
②⑩② 【读写分离】Sharding - JDBC 实现 MySQL读写分离[SpringBoot框架]
42 0
|
5月前
|
Java 关系型数据库 MySQL
数据库系列课程(04)-SpringBoot整合MySQL读写分离
数据库系列课程(04)-SpringBoot整合MySQL读写分离
75 0
|
5月前
|
存储 Java 数据库连接
Spring Boot 配置主从数据库实现读写分离
Spring Boot 配置主从数据库实现读写分离
181 0
|
7月前
|
Java 数据库 Spring
springboot 读写分离
springboot 读写分离
96 0
|
10月前
|
关系型数据库 MySQL Java
Springboot + Mysql8实现读写分离
Springboot + Mysql8实现读写分离
133 0
|
10月前
|
关系型数据库 MySQL Java
SpringBoot+Mysql8实现读写分离
SpringBoot+Mysql8实现读写分离
124 0
|
druid Java API
Java:SpringBoot整合Sharding-Jdbc实现数据库读写分离
Java:SpringBoot整合Sharding-Jdbc实现数据库读写分离
132 0
|
SQL 存储 算法
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
1280 0
SpringBoot整合ShardingSphere实现分表分库&读写分离&读写分离+数据库分表
|
消息中间件 Java 数据库连接
SpringBoot读写分离配置与事务
SpringBoot读写分离配置与事务
542 0
|
监控 关系型数据库 MySQL
SpringBoot + ShardingSphere-JDBC 实现读写分离
上一篇我们用 ShardingSphere-Proxy实现了读写分离 ShardingSphere 实战之读写分离 这一次我们用 ShardingSphere-JDBC 来实现一下
SpringBoot + ShardingSphere-JDBC 实现读写分离