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.mzph.cn/news/23442.shtml

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

相关文章

HCIP---OSPF的优化

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一.汇总&#xff1a; 目的&#xff1a;减少骨干区域的LSA的更新量 作用&#xff1a;OSPF的…

CI/CD—Docker初入门学习

1 docker 了解 1 Docker 简介 Docker 是基于 Go 语言的开源应用容器虚拟化技术。Docker的主要目标是build、ship and run any app&#xff0c;anywhere&#xff0c;即通过对应用组件的封装、分发、部署、运行等生命周期的管理&#xff0c;达到应用组件级别的一次封装、到处运…

阿里云安全组设置

简介​ 云主机安全组必须打开如下端口&#xff1a; ssh&#xff1a;22http&#xff1a;80https&#xff1a;443ftp&#xff1a;21、20000&#xff5e;30000 阿里云安全组端口开放教程​ 腾讯云安全组端口开放教程​ 华为云安全组端口开放教程​

一些网络知识总结(自用)

一些网络知识总结&#xff08;自用&#xff09; 1. 进制的转换 所有进制转换成十进制就是把字面值*权数 比如16进制 5AEF 转成10机制话就是 1* 151614321064 * 5 2. ip地址网段的概念&#xff0c;可用ip&#xff0c;广播地址 比如一个ip为10.1.1.1/24那么他的网络号就是前…

vue2 el-carousel轮播图和文字一起改变

vue项目的话 安装一下element依赖 npm i element-ui -S在main入口文件引入element包 我在app文件里边去写的 <template><div class"w"><el-carousel height"460px"><el-carousel-item v-for"item in items" :key"i…

优化|当机器学习上运筹学:PyEPO与端对端预测后优化

分享者&#xff1a;唐博 编者按&#xff1a;​ 这篇文章我想要写已经很久了&#xff0c;毕竟“端对端预测后优化”&#xff08;End-to-End Predict-then-Optimize&#xff09;正是我读博期间的主要研究方向&#xff0c;但我又一直迟迟没能下笔。想说自己杂事缠身&#xff08;实…

PCL点云处理之最小二乘空间直线拟合(3D) (二百零二)

PCL点云处理之最小二乘空间直线拟合(3D) (二百零二) 一、算法简介二、实现代码三、效果展示一、算法简介 对于空间中的这样一组点:大致呈直线分布,散乱分布在直线左右, 我们可采用最小二乘方法拟合直线,更进一步地,可以通过点到直线的投影,最终得到一组严格呈直线分布…

npm install报错 -> npm ERR! Unexpected token ‘.‘ 报错解决办法

问题原因&#xff1a; 用nvm1.1.7的版本安装了16.x以上的node, 然后再下载依赖的时候就报错了&#xff1b;总结一下就是nvm版本太低了&#xff0c;他的里面没有集成高版本node导致的。 解决办法&#xff1a; 把nvm切换到新版本就行了。 1. 卸载掉当前所有的node nvm unins…

[每日习题]第一个只出现一次的字符 小易的升级之路——牛客习题

hello,大家好&#xff0c;这里是bang___bang_&#xff0c;本篇记录2道牛客习题&#xff0c;第一个只出现一次的字符&#xff08;简单&#xff09;&#xff0c;小易的升级之路&#xff08;简单&#xff09;&#xff0c;如有需要&#xff0c;希望能有所帮助&#xff01; 目录 1️…

COMSOL SMS结构模拟简要步骤

做光纤传感方向的朋友们在日常的工作与学习中都想对你自己的结构进行一个仿真与模拟&#xff0c;以用于验证自己的思路与想法&#xff0c;又或者是在平时的文章中加入模拟以丰富自己的工作使得文章显得更加饱满&#xff0c;但又苦于在光纤传感方向的comsol案例、资料比较少&…

Appium+Python3环境搭建,其实超简单!

appium可以说是做app最火的一个自动化框架&#xff0c;它的主要优势是支持android和ios&#xff0c;另外脚本语言也是支持java和Python。略懂Python&#xff0c;所以接下来的教程是appiumpython&#xff0c;自己搭建环境的时候&#xff0c;按照某些博客安装遇到各种奇葩问题&am…

UE4 Cesium for unreal 离线加载应用全流程

参考配置&#xff1a;Win10、请保证是在局域网环境下配置 配置IP 右键选择&#xff1a;打开“网络和Internet” 设置 选择更改适配器选项 请保证以太网是处于启用状态并连接线缆&#xff0c;点击右键选择属性 双击选择Internet协议版本4&#xff08;TCP/IPv4&#xff09; 将IP地…

【雕爷学编程】MicroPython动手做(31)——物联网之Easy IoT

1、物联网的诞生 美国计算机巨头微软(Microsoft)创办人、世界首富比尔盖茨&#xff0c;在1995年出版的《未来之路》一书中&#xff0c;提及“物物互联”。1998年麻省理工学院提出&#xff0c;当时被称作EPC系统的物联网构想。2005年11月&#xff0c;国际电信联盟发布《ITU互联网…

常用消息中间件对比

Kafka 1.基于Pull的模式来处理消息消费 2.追求高吞吐量 3.一开始的目的就是日志收集和传输 4.0.8版本开始支持复制&#xff0c;不支持事务&#xff0c;对消息的重复、丢失、错误没有严格要求、适合产生大量数据的互联网服务的数据收集业务. RabbitMQ RabbitMQ是使用Erlang语…

Grafana集成prometheus(1.Prometheus安装)

下载docker镜像 docker pull prom/prometheus docker pull prom/node-exporter启动 node-exporter 该程序用以采集机器内存等数据 启动脚本 docker run -d -p 9100:9100 prom/node-exporter ss -anptl | grep 9100启动截图 prometheus 启动脚本 # 3b907f5313b7 为镜像i…

面试热题(x的平方根)

给你一个非负整数 x &#xff0c;计算并返回 x 的 算术平方根 。 由于返回类型是整数&#xff0c;结果只保留 整数部分 &#xff0c;小数部分将被 舍去 。 注意&#xff1a;不允许使用任何内置指数函数和算符&#xff0c;例如 pow(x, 0.5) 或者 x ** 0.5 。 这道题虽然是简单题…

【导出Word】如何使用Java+Freemarker模板引擎,根据XML模板文件生成Word文档(只含文本内容的模板)

这篇文章&#xff0c;主要介绍如何使用JavaFreemarker模板引擎&#xff0c;根据XML模板文件生成Word文档。 目录 一、导出Word文档 1.1、基础知识 1.2、制作模板文件 1.3、代码实现 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;创建Freemarker工具类 &…

.net 6 efcore一个model映射到多张表(非使用IEntityTypeConfiguration)

现在有两张表&#xff0c;结构一模一样&#xff0c;我又不想创建两个一模一样的model&#xff0c;就想一个model映射到两张表 废话不多说直接上代码 安装依赖包 创建model namespace oneModelMultiTable.Model {public class Test{public int id { get; set; }public string…

面试热题(前中序遍历构建树)

给定两个整数数组 preorder 和 inorder &#xff0c;其中 preorder 是二叉树的先序遍历&#xff0c; inorder 是同一棵树的中序遍历&#xff0c;请构造二叉树并返回其根节点。 题目中是给定两个数组&#xff0c;一个是存放这颗树的前序遍历的数组&#xff0c;一个是存放这棵树的…

如何把pdf转成cad版本?这种转换方法非常简单

将PDF转换成CAD格式的优势在于&#xff0c;CAD格式通常是用于工程设计和绘图的标准格式。这种格式的文件可以在计算机上进行编辑和修改&#xff0c;而不需要纸质副本。此外&#xff0c;CAD文件通常可以与其他CAD软件进行交互&#xff0c;从而使得工程设计和绘图过程更加高效和精…