当前位置: 首页 > article >正文

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

相互学习、共同进步...


http://www.kler.cn/a/471314.html

相关文章:

  • Pytorch初学
  • 微信小程序之历史上的今天
  • Taro地图组件和小程序定位
  • [文献精汇]使用PyCaret预测 Apple 股价
  • R语言的网络编程
  • http源码分析
  • Ollama + FastGPT搭建本地私有企业级AI知识库 (Linux)
  • php将word转换为pdf
  • 使用Oracle的RPM包在Linux上安装MYSQL
  • 《探索 OpenCV 4.10.0:计算机视觉领域的璀璨新星》
  • docker学习记录:本地部署mongodb
  • K8S集群更新api-sever证书的SAN属性
  • 【11_只出现一次的数字】
  • flink的EventTime和Watermark
  • Couldn‘t resolve host name for http://mirrorlist.centos.org
  • 《Swift 结构体》
  • 基于SMT32U575RIT单片机-中断练习
  • 【蓝桥杯——物联网设计与开发】Part2:OLED
  • 债券回购简述
  • HTTP 协议中,GET、PUT、POST、DELETE、OPTIONS 和 PATCH 区别
  • Git 常用命令及其使用场景
  • RabbitMQ ubuntu 在线安装
  • _decl_class_registry 与 metadata.sorted_tables的区别
  • 以柔资讯-D-Security终端文件保护系统 logFileName 任意文件读取漏洞复现
  • 机器学习——线性回归
  • 运动相机拍摄的视频打不开怎么办