SpringBoot读写分离

2023-12-25 18:45:00 浏览数 (1)

前提

  • 2台mysql

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

代码语言:javascript复制
 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

目录结构

maven依赖

代码语言:javascript复制
    <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

代码语言:javascript复制
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

代码语言:javascript复制
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位置,事务管理器等

代码语言:javascript复制
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 枚举类
代码语言:javascript复制
package com.example.enums;

/**
 * @author chaird
 * @create 2020-12-30 21:28
 */
public enum DBTypeEnum {
  MASTER,
  SLAVE;
}
MyRoutingDataSource

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

代码语言:javascript复制
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

设置切面和切面方法

代码语言:javascript复制
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方法出现主库

代码语言:javascript复制
package com.example.annotation;

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

给threadLocal设置参数

代码语言:javascript复制
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层

代码语言:javascript复制
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层

代码语言:javascript复制
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);
  }
}

接口层

代码语言:javascript复制
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";
  }
}

启动类

代码语言:javascript复制
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

总结

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

2)AOP也是秀

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

参考

SpringBoot MyBatis MySQL读写分离 - 废物大师兄 - 博客园

Spring 如何实现多数据源读写分离?

0 人点赞