前提
- 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
目录结构
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 | |
*/ | |
public class DataSourceConfig { | |
"spring.datasource.master") | (|
public DataSource masterDataSource() { | |
return DataSourceBuilder.create().build(); | |
} | |
"spring.datasource.slave") | (|
public DataSource slaveDataSource() { | |
return DataSourceBuilder.create().build(); | |
} | |
public DataSource myRoutingDataSource( | |
"masterDataSource") DataSource masterDataSource, | (|
"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; | |
public class MyBatisConfig { | |
private DataSource myRoutingDataSource; | |
public SqlSessionFactory sqlSessionFactory() throws Exception { | |
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); | |
sqlSessionFactoryBean.setDataSource(myRoutingDataSource); | |
sqlSessionFactoryBean.setMapperLocations( | |
new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); | |
return sqlSessionFactoryBean.getObject(); | |
} | |
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 { | |
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; | |
/** 切面表达式和方法 */ | |
public class DataSourceAop { | |
/** 读切面(条件1&&条件2) | |
* 条件1:没有Master注解 条件2:com.example.service包下 任意类 的select* 或者get*方法 | |
* */ | |
public void readPointcut() {} | |
/** 写切面 | |
* 类似上面,不做解释 | |
* */ | |
public void writePointcut() {} | |
/** | |
* Before方法,设置ThreadLocal里的一个变量为slave | |
*/ | |
public void read() { | |
DBContextHolder.slave(); | |
} | |
/** | |
* Before方法,设置ThreadLocal里的一个变量为master | |
*/ | |
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 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 | |
*/ | |
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 | |
*/ | |
public class StudentService { | |
private StudentDao studentDao; | |
public List<Student> getAllSlave() { | |
return studentDao.selectByExample(new StudentExample()); | |
} | |
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 | |
*/ | |
public class StudentController { | |
private StudentService studentService; | |
public Object getAllMaster() { | |
return studentService.getAllMaster(); | |
} | |
public Object getAllSlave() { | |
return studentService.getAllSlave(); | |
} | |
public Object save( 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; | |
"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