Springboot 读写分离
因为项目中需要用到读写分离,所以在网上找资料整理了下,主要采用AbstractRoutingDataSource+aop的方式实现,直接上代码。
代码结构图:
pom文件:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/>
</parent>
<groupId>org.readandwrite</groupId>
<artifactId>read-write</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>read-write</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.26</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
数据源配置文件:
spring:
datasource:
master:
jdbc-url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
druid:
initial-size: 5 #连接池初始化大小
min-idle: 10 #最小空闲连接数
max-active: 20 #最大连接数
web-stat-filter:
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据
stat-view-servlet: #访问监控网页的登录用户名和密码
login-username: druid
login-password: druid
slave1:
jdbc-url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
slave2:
jdbc-url: jdbc:mysql://127.0.0.1:3306/db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
mybatis:
# 搜索指定包别名
mapper-locations: classpath*:dao/*.xml
type-aliases-package: org.**.domain
数据源配置类:DataSourceConfig
package org.readandwrite.config;
import org.readandwrite.enums.DbEnum;
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 zwm
* @Date 2024/12/26 13:28
*/
@Configuration
public class DataSourceConfig {
//主数据源,用于写数据,特殊情况下也可用于读
@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 routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource){
Map<Object,Object> targetDataSource=new HashMap<>();
targetDataSource.put(DbEnum.MASTER,masterDataSource);
targetDataSource.put(DbEnum.SLAVE1,slave1DataSource);
targetDataSource.put(DbEnum.SLAVE2,slave2DataSource);
RoutingDataSource routingDataSource=new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(masterDataSource);
routingDataSource.setTargetDataSources(targetDataSource);
return routingDataSource;
}
}
数据源切换:DBContextHolder
package org.readandwrite.config;
import org.readandwrite.enums.DbEnum;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
public class DBContextHolder {
private static final ThreadLocal<DbEnum> contextHolder=new ThreadLocal<>();
private static final AtomicInteger counter=new AtomicInteger(-1);
public static void set(DbEnum type){
contextHolder.set(type);
}
public static DbEnum get(){
return contextHolder.get();
}
public static void master()
{
set(DbEnum.MASTER);
System.out.println("切换到master数据源");
}
public static void slave(){
//轮询数据源进行读操作
int index=counter.getAndIncrement() % 2;
if(counter.get()>9999){
counter.set(-1);
}
if(index==0){
set(DbEnum.SLAVE1);
System.out.println("切换到slave1数据源");
}else {
set(DbEnum.SLAVE2);
System.out.println("切换到slave2数据源");
}
}
}
确定当前数据源:RoutingDataSource,继承了AbstractRoutingDataSource,重写里面的determineCurrentLookupKey方法
package org.readandwrite.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
public class RoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
mybatis配置类:MybatisConfig
package org.readandwrite.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.ApplicationContext;
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;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@EnableTransactionManagement
@Configuration
@MapperScan({"org.readandwrite.dao"})
public class MybatisConfig {
@Resource(name = "routingDataSource")
private DataSource routingDataSource;
@Bean
public SqlSessionFactory sessionFactory(ApplicationContext applicationContext) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(routingDataSource);
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(routingDataSource);
}
}
数据源枚举:DbEnum
package org.readandwrite.enums;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
public enum DbEnum {
MASTER,SLAVE1,SLAVE2;
}
注解:Master
package org.readandwrite.annotation;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
public @interface Master {
}
AOP切入处理类:DataSourceAop。里面的解释应该写的很清楚了,根据自己的实际需求做出相应的调整即可。
package org.readandwrite.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.readandwrite.config.DBContextHolder;
import org.springframework.stereotype.Component;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@Aspect
@Component
public class DataSourceAop {
// 没有Master注解且service方法名以(select、get)开头
@Pointcut("!@annotation(org.readandwrite.annotation.Master)" +
" && (" +
" execution(* org.readandwrite.service..*.select*(..))" +
" || execution(* org.readandwrite.service..*.get*(..))" +
")" +
"")
public void readPointcut() {
}
// 有Master注解或者service方法名以(insert、add、update、edit、delete、remove)开头
@Pointcut("@annotation(org.readandwrite.annotation.Master) " +
"|| execution(* org.readandwrite.service..*.insert*(..)) " +
"|| execution(* org.readandwrite.service..*.add*(..)) " +
"|| execution(* org.readandwrite.service..*.update*(..)) " +
"|| execution(* org.readandwrite.service..*.edit*(..)) " +
"|| execution(* org.readandwrite.service..*.delete*(..)) " +
"|| execution(* org.readandwrite.service..*.remove*(..))")
public void writePointcut() {
}
@Before("readPointcut()")
public void read() {
DBContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
}
实体类:User
package org.readandwrite.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String orderNo;
private String orderTitle;
}
Mapper:UserMapper
package org.readandwrite.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.readandwrite.domain.User;
import java.util.List;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@Mapper
public interface UserMapper {
public List<User> selectAllUser();
public Integer insertUser(@Param("user") User user);
public User selectOneById(@Param("id") Integer id);
}
service:UserService
package org.readandwrite.service;
import org.readandwrite.annotation.Master;
import org.readandwrite.dao.UserMapper;
import org.readandwrite.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> getAllUser() {
return userMapper.selectAllUser();
}
@Master
public List<User> getMasterAll() {
return userMapper.selectAllUser();
}
public Integer addUser(User user) {
return userMapper.insertUser(user);
}
/*
* 特殊情况下,需要从主库查询时
* 例如某些业务更新数据后需要马上查询,因为主从复制有延迟,所以需要从主库查询
* 添加@Master注解即可从主库查询
*
* 该注解实现比较简单,在aop切入表达式中进行判断即可
* */
@Master
public User selectOneById(Integer id) {
return userMapper.selectOneById(id);
}
}
Controller:UserController
package org.readandwrite.controller;
import org.readandwrite.domain.User;
import org.readandwrite.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/getSlaveAll")
public List<User> getAll() {
return userService.getAllUser();
}
@GetMapping("/getMasterAll")
public List<User> getMasterAll() {
return userService.getMasterAll();
}
@PostMapping("/addUser")
public void addUser(@RequestBody User user) {
userService.addUser(user);
}
}
在浏览器直接调接口:http://localhost:8080/user/getSlaveAll
如果看到这,那么恭喜你,基于AbstractRoutingDataSource的读写分离方案就实现了。但是我相信很多人应该也会跟我一样报下面这个错误:
这个错误的根本原因是没有加载到xml配置文件,处理方法就是在注入SqlSessionFactory的时候收手动去加载资源文件。请修改MybatisConfig类的代码如下:
package org.readandwrite.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.ApplicationContext;
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;
/**
* @Author zwm
* @Date 2024/12/26 13:28
*/
@EnableTransactionManagement
@Configuration
@MapperScan({"org.readandwrite.dao"})
public class MybatisConfig {
@Resource(name = "routingDataSource")
private DataSource routingDataSource;
@Bean
public SqlSessionFactory sessionFactory(ApplicationContext applicationContext) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(routingDataSource);
// PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// org.springframework.core.io.Resource[] resources = resolver.getResources("classpath*:dao/*.xml");
// sqlSessionFactoryBean.setMapperLocations(resources);
//如果报别名没有找打请加上这
// sqlSessionFactoryBean.setTypeAliasesPackage("org.**.domain");
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath*:dao/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(routingDataSource);
}
}
注释的部分是另一种加载方式,也是可以的,根据自己的喜爱来。
至此,整个读写分离方案基本就结束了。
如果嫌自己配置的麻烦,也可以直接下载:https://download.csdn.net/download/javaweiming/90234902
相互学习、共同进步...