0
点赞
收藏
分享

微信扫一扫

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​​

目录结构

SpringBoot读写分离_sql

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

SpringBoot读写分离_spring_02

总结

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

2)AOP也是秀

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

参考

​​SpringBoot+MyBatis+MySQL读写分离 - 废物大师兄 ​​

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

举报

相关推荐

0 条评论