Spring Boot实现方式
读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,一般来讲,主要有两种实现方式,分别为:
1.使用中间件,比如Atlas,cobar,TDDL,mycat,heisenberg,Oceanus,vitess,OneProxy等
2.使用程序自己实现,利用Spring Boot提供的路由数据源以及AOP,实现起来简单快捷(本文要介绍的方法)
代码实现
1.首先配置下pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
2.数据源路由类功能RoutingDataSource.java
package com.example.demo.databases;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 数据源路由类功能
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DBContext.get();
}
}
3.数据源上下文类DBContext.java
package com.example.demo.databases;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 数据源上下文类
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
@Slf4j
public class DBContext {
private static final ThreadLocal<DBTypeEnum> dbContext = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum dbType) {
dbContext.set(dbType);
}
public static DBTypeEnum get() {
return dbContext.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
log.info("切换到master库");
}
public static void slave() {
// 读库负载均衡(轮询方式)
int index = counter.getAndIncrement() % 2;
log.info("slave库访问线程数==>{}", counter.get());
if (index == 0) {
set(DBTypeEnum.SLAVE1);
log.info("切换到slave1库");
} else {
set(DBTypeEnum.SLAVE2);
log.info("切换到slave2库");
}
}
}
4.数据库枚举类DBTypeEnum.java
package com.example.demo.databases;
/**
* 数据库枚举类
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
public enum DBTypeEnum {
MASTER, SLAVE1, SLAVE2
}
这里我们配置三个库,分别是一个写库Master,2个读库slave1,slave2
5.数据库配置类DataSourceConfig.java
package com.example.demo.databases;
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;
/**
* 数据库配置类
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
@Configuration
public class DataSourceConfigs {
@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
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(masterDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
6.mybatis配置类DataSourceConfigs.java
package com.example.demo.databases;
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;
/**
* 数据库配置类
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
@Configuration
public class DataSourceConfigs {
@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
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(masterDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
7.切面类DataSourceAop.java
package com.example.demo.databases;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面类DataSourceAop
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
@Aspect
@Component
public class DataSourceAop {
@Pointcut("@annotation(com.example.demo.databases.Master) " +
"|| execution(* com.example.demo.*.service..*.insert*(..)) " +
"|| execution(* com.example.demo.*.service..*.create*(..)) " +
"|| execution(* com.example.demo.*.service..*.save*(..)) " +
"|| execution(* com.example.demo.*.service..*.add*(..)) " +
"|| execution(* com.example.demo.*.service..*.update*(..)) " +
"|| execution(* com.example.demo.*.service..*.edit*(..)) " +
"|| execution(* com.example.demo.*.service..*.delete*(..)) " +
"|| execution(* com.example.demo.*.service..*.remove*(..))")
public void writePointcut() {
}
@Pointcut("!@annotation(com.example.demo.databases.Master) " +
"&& (execution(* com.example.demo.*.service..*.select*(..)) " +
"|| execution(* com.example.demo.*.service..*.list*(..))" +
"|| execution(* com.example.demo.*.service..*.count*(..))" +
"|| execution(* com.example.demo.*.service..*.get*(..)))"
)
public void readPointcut() {
}
@Before("writePointcut()")
public void write() {
DBContext.master();
}
@Before("readPointcut()")
public void read() {
DBContext.slave();
}
}
8.注解类Master.java
package com.example.demo.databases;
/**
* 注解类Master 主库,可读写
* @author fansongsong
* @since 2020-09-08
* @version 1.0
*/
public @interface Master {
}
9,验证
请求查询接口
指定数据源写库(查询默认读库,可以通过@Master注解加到方法上手动指定写库)
请求新增方法
请求批量新增用户接口
查看数据源切换到主库
验证数据主从同步成功