SpringBoot实现数据库读写分离

news2024/11/24 8:57:30

SpringBoot实现数据库读写分离

参考博客https://blog.csdn.net/qq_31708899/article/details/121577253
实现原理:翻看AbstractRoutingDataSource源码我们可以看到其中的targetDataSource可以维护一组目标数据源(采用map数据结构),并且做了路由key与目标数据源之间的映射,提供基于key查找数据源的方法。看到了这个,我们就可以想到怎么实现数据源切换了
在这里插入图片描述
在这里插入图片描述#### 一 maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<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 https://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.4.12-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ReadAndWriteSeparate</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ReadAndWriteSeparate</name>
    <description>Demo project for Spring Boot</description>
    <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>
            <optional>true</optional>
        </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>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>
                        **/*.xml
                    </include>
                </includes>
            </resource>
        </resources>
    </build>

</project>

二 数据源配置

  1. yaml配置
    这里我只用一个账号模拟,生产环境下必须要分开只读账号和可读可写账号,因为主从复制中,主机可不会同步从机的数据哟
spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver
    slave2:
      jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: root
      driver-class-name: com.mysql.jdbc.Driver

  1. 数据源配置
package com.readandwriteseparate.demo.Config;

import com.readandwriteseparate.demo.Enum.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.beans.ConstructorProperties;
import java.util.HashMap;
import java.util.Map;

/**
 * @author OriginalPerson
 * @date 2021/11/25 20:25
 * @Email 2568500308@qq.com
 */
@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;
    }

}


这里我们配置了4个数据源,其中前三个数据源都是为了生成第四个路由数据源产生的,路由数据源的key我们使用枚举类型来标注,三个枚举类型分别代表数据库的类型。

package com.readandwriteseparate.demo.Enum;

/**
 * @author OriginalPerson
 * @date 2021/11/25 20:45
 * @Email: 2568500308@qq.com
 */
public enum DbEnum {
    MASTER,SLAVE1,SLAVE2;
}

三 数据源切换

这里我们使用ThreadLocal将路由key设置到每个线程的上下文中这里也进行一个简单的负载均衡,轮询两个只读数据源,而访问哪个取决于counter的值,每增加1,切换一下数据源,该值为juc并发包下的原子操作类,保证其线程安全。

  1. 设置路由键,获取当前数据源的key
package com.readandwriteseparate.demo.Config;

import com.readandwriteseparate.demo.Enum.DbEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author OriginalPerson
 * @date 2021/11/25 20:49
 * @Email: 2568500308@qq.com
 */
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数据源");
        }
    }
}

  1. 确定当前数据源

这个比较重要,其继承AbstractRoutingDataSource类,重写了determineCurrentLookupKey方法,该方法决定当前数据源的key,对应于上文配置数据源的map集合中的key,让该方法返回我们定义的ThreadLocal中存储的key,即可实现数据源切换。


import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

/**
 * @author OriginalPerson
 * @date 2021/11/25 20:47
 * @Email: 2568500308@qq.com
 */
public class RoutingDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}

  1. mybatis配置三个数据源
package com.readandwriteseparate.demo.Config;

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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * @author OriginalPerson
 * @date 2021/11/25 22:17
 * @Email 2568500308@qq.com
 */
@EnableTransactionManagement
@Configuration
public class MybatisConfig {

    @Resource(name = "routingDataSource")
    private DataSource routingDataSource;

    @Bean
    public SqlSessionFactory sessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(routingDataSource);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager(){
        return new DataSourceTransactionManager(routingDataSource);
    }
}

四 特殊处理master主库读数据操作、

在某些场景下,我们需要实时读取到更新过的值,例如某个业务逻辑,在插入一条数据后,需要立即查询据,因为读写分离我们用的是主从复制架构,它是异步操作,串行复制数据,所以必然存在主从延迟问题,对于刚插入的数据,如果要马上取出,读从库是没有数据的,因此需要直接读主库,这里我们通过一个Master注解来实现,被该注解标注的方法将直接在主库数据

  1. 注解
package com.readandwriteseparate.demo.annotation;

/**
 * @author OriginalPerson
 * @date 2021/11/26 13:28
 * @Email 2568500308@qq.com
 */


public @interface Master {
}

  1. APO切面处理
package com.readandwriteseparate.demo.Aspect;

import com.readandwriteseparate.demo.Config.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @author OriginalPerson
 * @date 2021/11/26 13:23
 * @Email 2568500308@qq.com
 */

@Aspect
@Component
public class DataSourceAop {
   
   // 非master注解且有关读的方法操作从库
   @Pointcut("!@annotation(com.readandwriteseparate.demo.annotation.Master)" +
            " && (execution(* com.readandwriteseparate.demo.Service..*.select*(..)))" +
            " || execution(* com.readandwriteseparate.demo.Service..*.get*(..)))")
    public void readPointcut(){

    }

  
  // 有master注解或者有关处理数据的操作主库  @Pointcut("@annotation(com.readandwriteseparate.demo.annotation.Master) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.insert*(..)) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.add*(..)) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.update*(..)) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.edit*(..)) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.delete*(..)) " +
            "|| execution(* com.readandwriteseparate.demo.Service..*.remove*(..))")
    public void writePointcut() {

    }

    @Before("readPointcut()")
    public void read(){
        DBContextHolder.slave();
    }

    @Before("writePointcut()")
    public void write(){
        DBContextHolder.master();
    }
}

五 读写分离案例使用

  1. 实体类
package com.readandwriteseparate.demo.Domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author OriginalPerson
 * @date 2021/11/26 23:15
 * @Email 2568500308@qq.com
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private Integer id;
    private String name;
    private String sex;
}

  1. Dao层
package com.readandwriteseparate.demo.Dao;

import com.readandwriteseparate.demo.Domain.User;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @author OriginalPerson
 * @date 2021/11/26 23:16
 * @Email 2568500308@qq.com
 */
public interface UserMapper {
    public List<User> selectAllUser();

    public Integer insertUser(@Param("user") User user);

    public User selectOneById(@Param("id") Integer id);
}

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.readandwriteseparate.demo.Dao.UserMapper">

    <resultMap id="user" type="com.readandwriteseparate.demo.Domain.User">
        <id property="id" column="id"></id>
        <result property="name" column="name"></result>
        <result property="sex" column="sex"></result>
    </resultMap>

    <select resultMap="user" id="selectAllUser" resultType="com.readandwriteseparate.demo.Domain.User">
        select * from user
    </select>

    <insert id="insertUser" parameterType="com.readandwriteseparate.demo.Domain.User">
        insert into user(name,sex) values(#{user.name},#{user.sex})
    </insert>

    <select id="selectOneById" parameterType="java.lang.Integer" resultMap="user">
        select * from user where id=#{id}
    </select>
</mapper>

service

package com.readandwriteseparate.demo.Service;

import com.readandwriteseparate.demo.Dao.UserMapper;
import com.readandwriteseparate.demo.Domain.User;
import com.readandwriteseparate.demo.annotation.Master;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author OriginalPerson
 * @date 2021/11/27 0:07
 * @Email 2568500308@qq.com
 */

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> getAllUser(){
        return userMapper.selectAllUser();
    }

    public Integer addUser(User user){
        return userMapper.insertUser(user);
    }

    /*
    * 特殊情况下,需要从主库查询时
    * 例如某些业务更新数据后需要马上查询,因为主从复制有延迟,所以需要从主库查询
    * 添加@Master注解即可从主库查询
    *
    * 该注解实现比较简单,在aop切入表达式中进行判断即可
    * */
    @Master
    public User selectOneById(Integer id){
        return userMapper.selectOneById(id);
    }
}

单元测试代码

package com.readandwriteseparate.demo;

import com.readandwriteseparate.demo.Dao.UserMapper;
import com.readandwriteseparate.demo.Domain.User;
import com.readandwriteseparate.demo.Service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ReadAndWriteSeparateApplicationTests {

    @Autowired
    private UserService userService;
    @Test
    void contextLoads() throws InterruptedException {
        User user=new User();
        user.setName("赵六");
        user.setSex("男");
        System.out.println("插入一条数据");
        userService.addUser(user);
        for (int i = 0; i <4 ; i++) {
            System.out.println("开始查询数据");
            System.out.println("第"+(i+1)+"次查询");
            userService.getAllUser();
            System.out.println("-------------------------分割线------------------------");
        }
        System.out.println("强制查询主库");
        userService.selectOneById(1);
    }

}

查询结果:
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/839725.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《华为认证》SR-MPLS-TE

实验需求&#xff1a;运营商网络配置SR-MPLS-TE&#xff0c;实现CE1和CE2之间的互访流量通过PE1-P2-P4-PE3。 步骤1&#xff1a;配置运营商网络的IGP协议&#xff08;本实验采用ISIS协议&#xff09; PE1&#xff1a; isis 1is-level level-2cost-style widenetwork-entity 49…

一个.NET开发的Web版Redis管理工具

今天给大家推荐一款web 版的Redis可视化工具WebRedisManager&#xff0c;即可以作为单机的web 版的Redis可视化工具来使用&#xff0c;也可以挂在服务器上多人管理使用的web 版的Redis可视化工具。 WebRedisManager基于SAEA.Socket通信框架中的SAEA.RedisSocket、SAEA.WebApi两…

Python实现决策树算法:完整源码逐行解析

决策树是一种常用的机器学习算法&#xff0c;它可以用来解决分类和回归问题。决策树的优点是易于理解和解释&#xff0c;可以处理数值和类别数据&#xff0c;可以处理缺失值和异常值&#xff0c;可以进行特征选择和剪枝等操作。决策树的缺点是容易过拟合&#xff0c;对噪声和不…

云原生应用里的服务发现

服务定义&#xff1a; 服务定义是声明给定服务如何被消费者/客户端使用的方式。在建立服务之间的同步通信通道之前&#xff0c;它会与消费者共享。 同步通信中的服务定义&#xff1a; 微服务可以将其服务定义发布到服务注册表&#xff08;或由微服务所有者手动发布&#xff09;…

内网穿透:ngrok使用教程

一、前言 平时我们在本地8080端口创建一个服务的时候&#xff0c;都是使用localhost:8080访问我们的web服务。但是外网是不能访问我们的web服务的。这时&#xff0c;如果你要实现外网访问的功能就需要实现内网穿透&#xff0c;ngrok就是可以帮我们实现这个功能。 二、ngrok介…

岩土工程仪器多通道振弦传感器信号转换器应用于隧道安全监测

岩土工程仪器多通道振弦传感器信号转换器应用于隧道安全监测 多通道振弦传感器信号转换器VTI104_DIN 是轨道安装式振弦传感器信号转换器&#xff0c;可将振弦、温度传感器信号转换为 RS485 数字信号和模拟信号输出&#xff0c;方便的接入已有监测系统。 传感器状态 专用指示灯方…

unraid docker桥接模式打不开页面,主机模式正常

unraid 80x86版filebrowser&#xff0c;一次掉电后&#xff0c;重启出现权限问题&#xff0c;而且filebrowser的核显驱动不支持amd的VA-API 因为用不上核显驱动&#xff0c;解压缩功能也用不上&#xff0c;官方版本的filebrowser还小巧一些&#xff0c;18m左右 安装的时候总是…

QTableWidget对单元格(QWidget/QTableWidgetItem)的内存管理[clearContents()]

目录 现象结论代码验证clearContents() 会释放QTableWidgetItem 和QWidget 对象&#xff0c;但是不指向nullptrmemorytable.hmemorytable.cpp断点情况 验证clearContents()是延时释放QWidget 的而QTableWidgetItem 立即释放 现象 结论 clearContents() 会清除表格中的所有单元格…

小程序 view下拉滑动导致scrollview滑动事件失效

小程序页面需要滑动功能 下拉时滑动&#xff0c;展示整个会员卡内容&#xff0c; 下拉view里包含了最近播放&#xff1a;有scrollview&#xff0c;加了下拉功能后&#xff0c;scrollview滑动失败了。 <view class"cover-section" catchtouchstart"handletou…

eNSP:ospf和mgre的配置

实验要求&#xff1a; 第一步&#xff1a;路由、IP的配置 r1&#xff1a; <Huawei>sys Enter system view, return user view with CtrlZ. [Huawei]sys r1 [r1]int loop0 [r1-LoopBack0]ip add 192.168.1.1 24 [r1-LoopBack0]int g0/0/0 [r1-GigabitEthernet0/0/0]ip a…

部署Tomcat和jpress应用

静态页面&#xff1a;静态页面是指在服务器上提前生成好的HTML文件&#xff0c;每次用户请求时直接返回给用户。静态页面的内容是固定的&#xff0c;不会根据用户的请求或其他条件进行变化。静态页面的优点是加载速度快&#xff0c;对服务器资源要求较低&#xff0c;但缺点是无…

git报错:Error merging: refusing to merge unrelated histories

碰对了情人&#xff0c;相思一辈子。 打命令&#xff1a;git pull origin master --allow-unrelated-histories 然后等一会 再push 切记不要有冲突的代码 需要改掉~

Spring Cloud Eureka 和 zookeeper 的区别

CAP理论 在了解eureka和zookeeper区别之前&#xff0c;我们先来了解一下这个知识&#xff0c;cap理论。 1998年的加州大学的计算机科学家 Eric Brewer 提出&#xff0c;分布式有三个指标。Consistency&#xff0c;Availability&#xff0c;Partition tolerance。简称即为CAP。…

一则简单代码的汇编分析

先通过Xcode创建一个terminal APP&#xff0c;语言选择C。代码如下&#xff1a; #include <stdio.h>int main(int argc, const char * argv[]) {int a[7]{1,2,3,4,5,6,7};int *ptr (int*)(&a1);printf("%d\n",*(ptr));return 0; } 在return 0处打上断点&…

AcWing 24:机器人的运动范围 ← BFS、DFS

【题目来源】https://www.acwing.com/problem/content/description/22/【题目描述】 地上有一个 m 行和 n 列的方格&#xff0c;横纵坐标范围分别是 0∼m−1 和 0∼n−1。 一个机器人从坐标 (0,0) 的格子开始移动&#xff0c;每一次只能向左&#xff0c;右&#xff0c;上&#…

设计模式--策略模式(由简单工厂到策略模式到两者结合图文详解+总结提升)

目录 概述概念组成应用场景注意事项类图 衍化过程需求简单工厂实现图代码 策略模式图代码 策略模式简单工厂图代码 总结升华版本迭代的优化点及意义什么样的思路进行衍化的扩展思考--如何理解策略与算法 概述 概念 策略模式是一种行为型设计模式&#xff0c;它定义了算法家族&…

Docker安装Grafana以及Grafana应用

Doker基础 安装 1、 卸载旧的版本 sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine 2、需要的安装包 sudo yum install -y yum-utils 3、设置镜像的仓库 yum-config-m…

UML-构件图

目录 1.概述 2.构件的类型 3.构件和类 4.构件图 1.概述 构件图主要用于描述各种软件之间的依赖关系&#xff0c;例如&#xff0c;可执行文件和源文件之间的依赖关系&#xff0c;所设计的系统中的构件的表示法及这些构件之间的关系构成了构件图 构件图从软件架构的角度来描述…

数组的学习

数组学习 文章目录 数组来由数组的使用数组的内存图变量声明和args参数说明声明分配空间值的省略写法数组的length属性数列输出求和判断购物金额结算Arrays的sort和toString方法Arrays的equals和fill和copyOf和binarySearch方法字符数组顺序和逆序输出 数组来由 录入30个学生…

Gson:解析JSON为复杂对象:TypeToken

需求 通过Gson&#xff0c;将JSON字符串&#xff0c;解析为复杂类型。 比如&#xff0c;解析成如下类型&#xff1a; Map<String, List<Bean>> 依赖&#xff08;Gson&#xff09; <dependency><groupId>com.google.code.gson</groupId><art…