SpringBoot实现数据库读写分离

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/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave1:jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverslave2:jdbc-url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: rootdriver-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();}@Beanpublic 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@Overrideprotected 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;@Beanpublic SqlSessionFactory sessionFactory() throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean=new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(routingDataSource);return sqlSessionFactoryBean.getObject();}@Beanpublic 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 {@Autowiredprivate UserMapper userMapper;public List<User> getAllUser(){return userMapper.selectAllUser();}public Integer addUser(User user){return userMapper.insertUser(user);}/** 特殊情况下,需要从主库查询时* 例如某些业务更新数据后需要马上查询,因为主从复制有延迟,所以需要从主库查询* 添加@Master注解即可从主库查询** 该注解实现比较简单,在aop切入表达式中进行判断即可* */@Masterpublic 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 {@Autowiredprivate UserService userService;@Testvoid 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.rhkb.cn/news/75065.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小红书2023“家生活”趋势白皮书

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 近年来&#xff0c;年轻人与家的关系愈发紧密。 在小红书上&#xff0c;我们观察到了家居家装内容的蓬勃生长&#xff0c;3 年来相关内容的笔记规模增长了6倍&#xff0c;相关品类的搜索量增加的 3.…

大数据_Hadoop_Parquet数据格式详解

之前有面试官问到了parquet的数据格式&#xff0c;下面对这种格式做一个详细的解读。 参考链接 &#xff1a; 列存储格式Parquet浅析 - 简书 Parquet 文件结构与优势_parquet文件_KK架构的博客-CSDN博客 Parquet文件格式解析_parquet.block.size_davidfantasy的博客-CSDN博…

爆肝整理,性能测试-Jmeter测试+性能测试报告生成(超细整理)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 Jmeter执行原理 …

《吐血整理》进阶系列教程-拿捏Fiddler抓包教程(16)-Fiddler如何充当第三者再识AutoResponder标签-上

1.简介 Fiddler充当第三者&#xff0c;主要是通过AutoResponder标签在客户端和服务端之间&#xff0c;Fiddler抓包&#xff0c;然后改包&#xff0c;最后发送。AutoResponder这个功能可以算的上是Fiddler最实用的功能&#xff0c;可以让我们修改服务器端返回的数据&#xff0c…

ES6 - generator和async函数

一、前言 ES6 诞生以前&#xff0c;异步编程的方法&#xff0c;大概有下面四种。 回调函数事件监听发布/订阅Promise 对象 回调函数本身并没有问题&#xff0c;它的问题出现在多个回调函数嵌套会造成回调地狱&#xff0c;非常不利于代码的维护和逻辑混乱等问题&#xff1b; …

java linq多字段排序时间比较

public static void main(String[] args) {//100万条数据List<CrmInvestSaleUserCount> waitAssignUserList new ArrayList<>();for (int i 0; i < 1000000; i) {waitAssignUserList.add(new CrmInvestSaleUserCount().setSales_username("test" i…

C++stack_queue

stack_queue 容器适配器stack详解栈适配器栈模拟实现 队列详解队列适配器queue模拟实现 容器适配器 除了顺序容器外&#xff0c;标准库还定义了三个顺序容器适配器:stack(栈),queue(队列),priority_queue(优先队列)。适配器是标准库中的一个通用概念。容器&#xff0c;迭代器和…

非凸科技受邀参加中科大线上量化分享

7月30日&#xff0c;非凸科技受邀参加由中国科学技术大学管理学院学生会、超级量化共同组织的“打开量化私募的黑箱”线上活动&#xff0c;分享量化前沿以及求职经验&#xff0c;助力同学们拿到心仪的offer。 活动上&#xff0c;非凸科技量化策略负责人陆一洲从多个角度分享了如…

Vue基本语法

1. 官网&#xff1a; Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org) 一、示例代码 如下代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible&q…

骨传导耳机什么牌子好?盘点最受欢迎的几款骨传导耳机

骨传导耳机最近一两年越来越受欢迎&#xff0c;市场上不同形态的非入耳式耳机都有&#xff0c;从骨传导&#xff0c;夹耳式到气传导等等都有。骨传导耳机的好处有很多&#xff0c;非入耳式&#xff0c;不伤耳朵&#xff0c;佩戴更舒适更安全。但是一直以来&#xff0c;骨传导耳…

Qt 中引入ffmpeg 动态库

1、前期准备 在qt引入ffmpeg动态库的时候&#xff0c;需要准备ffmpeg的动态库和头文件。 2、打开qt项目 在qt项目的.pro文件中添加以下几行代码 INCLUDEPATH $$PWD/thirtLib/ffmpeg4.2/include win32: LIBS -L$$PWD/thirtLib/ffmpeg4.2/lib/ -lavcodec -lavdevice -lavf…

【SQL应知应会】表分区(三)• Oracle版

欢迎来到爱书不爱输的程序猿的博客, 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 本文收录于SQL应知应会专栏,本专栏主要用于记录对于数据库的一些学习&#xff0c;有基础也有进阶&#xff0c;有MySQL也有Oracle 分区表 • Oracle版 前言一、分区表1.什么是表分区…

搞个个人博客,纯学习想找个纯html模板咋就这难

以前做毕业设计的时候老想找一些不掺杂后端代码的前端模板。 可是下载下来&#xff0c;不是php就是python后台的。看又看不懂&#xff0c;想换语言就必须先把里面的后台代码拿掉。 就很像买了个精装的二手房&#xff0c;白白多花了砸墙钱。 就比如&#xff0c;想做个带菜单的…

Spring Boot集成Swagger3.0,Knife4j导出文档

文章目录 Spring Boot集成Swagger3.0,Knife4j导出文档效果展示如何使用简要说明添加依赖添加配置类测试接口token配置位置 官网 说明情况 demo Spring Boot集成Swagger3.0,Knife4j导出文档 效果展示 如何使用 简要说明 Knife4j的前身是swagger-bootstrap-ui,前身swagger-boo…

区块链实验室(14) - 编译FISCO-BCOS

FISCO-BCOS是一种区块链平台&#xff0c;与Hyperledger和Ethereum有些不同&#xff0c;详见FISCO BCOS 区块链 编译FISCO BCOS源码的目的是修改或者新增其中功能模块&#xff0c;进行对比实验&#xff0c;验证新想法、新创意的效果。编译的步骤很简单&#xff0c;按技术文档一…

stl_vector类(使用+实现)(C++)

vector 一、vector-简单介绍二、vector的常用接口1.常见构造2.iterator的使用3.容量操作4.增删查改操作5.迭代器失效问题6.动态二维数组 三、vector实现1.vector类重要的方法实现分析介绍(1)、涉及memcpy深浅拷贝问题(2)、成员变量 2.vector类整体实现代码 四、vector< char…

LUN映射出错导致写操作不互斥的服务器数据恢复案例

服务器数据恢复环境&#xff1a; 某公司的光纤SAN存储系统&#xff0c;6块硬盘组建一组RAID6&#xff0c;划分若干LUN&#xff0c;MAP到不同的SOLARIS操作系统服务器上。 服务器故障&分析&#xff1a; 由于业务增长需要新增应用&#xff0c;工作人员增加了一台IBM服务器&am…

uniapp小程序,根据小程序的环境版本,控制的显页面功能按钮的示隐藏

需求&#xff1a;根据小程序环境控制控制页面某个功能按钮的显示隐藏&#xff1b; 下面是官方文档和功能实现的相关代码&#xff1a; 实现上面需要&#xff0c;用到了uni.getAccountInfoSync()&#xff1a; uni.getAccountInfoSync() 是一个 Uniapp 提供的同步方法&#xff0c…

ArcGIS、ENVI、InVEST、FRAGSTATS等多技术融合提升环境、生态、水文、土地、土壤、农业、大气等领域的数据分析

一、 空间数据获取与制图 1.1 软件安装与应用讲解 1.2 空间数据介绍 1.3海量空间数据下载 1.4 ArcGIS软件快速入门 1.5 Geodatabase地理数据库 二、 ArcGIS专题地图制作 2.1专题地图制作规范 2.2 空间数据的准备与处理 2.3 空间数据可视化&#xff1a;地图符号与注记 …

【图论】差分约束

一.情景导入 x1-x0<9 ; x2-x0<14 ; x3-x0<15 ; x2-x1<10 ; x3-x2<9; 求x3-x0的最大值&#xff1b; 二.数学解法 联立式子2和5&#xff0c;可得x3-x0<23;但式子3可得x3-x0<15。所以最大值为15&#xff1b; 三.图论 但式子多了我们就不好解了&#xff0…