SpringBoot+MybatisPlus实现读写分离,自动切换数据源

读写分离有必要吗?

实现读写分离势必要与你所做的项目相关,如果项目读多写少,那就可以设置读写分离,让“读”可以更快,因为你可以把你的“读”数据库的innodb设置为MyISAM引擎,让MySQL处理速度更快。

实现读写分离的步骤

监听MybatisPlus接口,判断是写入还是读取

在这里我使用的是AOP的方式,动态监听MybatisPlus中Mapper的方法。

import com.supostacks.wrdbrouter.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MyBatisPlusAop {

    @Pointcut("execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.select*(..))")
    public void readPointCut(){
    }
	
    @Pointcut("execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.insert*(..))" +
              "||execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.update*(..))" +
              "||execution(* com.baomidou.mybatisplus.core.mapper.BaseMapper.delete*(..))")
    public void writePointCut(){
    }

    @Before("readPointCut()")
    public void readBefore(){
        DBContextHolder.setDBKey("dataread");
    }

    @Before("writePointCut()")
    public void writeBefore(){
        DBContextHolder.setDBKey("datawrite");
    }
}

定义介绍:
DBContextHolder中使用了ThreadLocal存储数据库名
readPointCut定义读的切点,如果调用的是BaseMapper.select*(…)则判断是读数据,则调用读库。
writePointCut定义写的切点,如果调用的是BaseMapper.insert|update|delete*(…)则判断是写数据,则调用写库

自定义MyBatis的DataSourceAutoConfiguration

DataSourceAutoConfiguration是Mybatis官方使用的SpringBootStarter,因为我这边自定义了Mybatis连接的相关属性名用来切换数据源,所以我需要自构一个DataSourceAutoConfig,代码如下:


@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {

    private final String TAG_GLOBAL = "global";
    /**
     * 数据源配置组
     */
    private final Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();

    /**
     * 默认数据源配置
     */
    private Map<String, Object> defaultDataSourceConfig;

    public DataSource createDataSource(Map<String,Object> attributes){
        try {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setUrl(attributes.get("url").toString());
            dataSourceProperties.setUsername(attributes.get("username").toString());
            dataSourceProperties.setPassword(attributes.get("password").toString());

            String driverClassName = attributes.get("driver-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("driver-class-name").toString();
            dataSourceProperties.setDriverClassName(driverClassName);

            String typeClassName = attributes.get("type-class-name") == null ? "com.zaxxer.hikari.HikariDataSource" : attributes.get("type-class-name").toString();
            return dataSourceProperties.initializeDataSourceBuilder().type((Class<DataSource>) Class.forName(typeClassName)).build();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }

    @Bean
    public DataSource createDataSource() {
        // 创建数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        for (String dbInfo : dataSourceMap.keySet()) {
            Map<String, Object> objMap = dataSourceMap.get(dbInfo);
            // 根据objMap创建DataSourceProperties,遍历objMap根据属性反射创建DataSourceProperties
            DataSource ds = createDataSource(objMap);
            targetDataSources.put(dbInfo, ds);
        }

        // 设置数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // db0为默认数据源
        dynamicDataSource.setDefaultTargetDataSource(createDataSource(defaultDataSourceConfig));

        return dynamicDataSource;
    }


    @Override
    public void setEnvironment(Environment environment) {
        String prefix = "wr-db-router.spring.datasource.";


        String datasource = environment.getProperty(prefix + "db");
        Map<String, Object> globalInfo = getGlobalProps(environment, prefix + TAG_GLOBAL);


        assert datasource != null;
        for(String db : datasource.split(",")){
            final String dbKey = prefix + db; //数据库列表
            Map<String,Object> datasourceProps = PropertyUtil.handle(environment,dbKey, Map.class);
            injectGlobals(datasourceProps, globalInfo);

            dataSourceMap.put(db,datasourceProps);
        }

        String defaultData = environment.getProperty(prefix + "default");
        defaultDataSourceConfig = PropertyUtil.handle(environment,prefix + defaultData, Map.class);
        injectGlobals(defaultDataSourceConfig, globalInfo);
    }

    public Map getGlobalProps(Environment env, String key){
        try {
            return PropertyUtil.handle(env,key, Map.class);
        } catch (Exception e) {
            return Collections.EMPTY_MAP;
        }
    }

    private void injectGlobals(Map<String,Object> origin,Map<String,Object> global){
        global.forEach((k,v)->{
            if(!origin.containsKey(k)){
                origin.put(k,v);
            }else{
                injectGlobals((Map<String, Object>) origin.get(k), (Map<String, Object>) global.get(k));
            }
        });
    }

DynamicDataSource 这个类继承了AbstractRoutingDataSource,通过获取ThreadLocal中的数据库名,动态切换数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Value("wr-db-router.spring.datasource.default")
    private String defaultDatasource;
    @Override
    protected Object determineCurrentLookupKey() {
        if(null == DBContextHolder.getDBKey()){
            return defaultDatasource;
        }else{
            return DBContextHolder.getDBKey();
        }
    }
}

我们通过重写determineCurrentLookupKey方法并设置对应的数据库名称,我们就可以实现切换数据源的功能了。

AbstractRoutingDataSource 主要源码如下:

 public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }

...

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

自定义MyBatisPlus的SpringBoot自动配置

MybatisPlus是默认使用的Mybatis的自带的DataSourceAutoConfiguration,但是我们已经将这个自定义了,所以我们也要去自定义一个MyBatisPlusAutoConfig,如果不自定义的话,系统启动将报错。代码如下:

@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfig.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MyBatisPlusAutoConfig  implements InitializingBean {
xxx
}

这个代码是直接拷贝了MyBatisPlusAutoConfiguration,只是将@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})改为了
@AutoConfigureAfter({DataSourceAutoConfig.class, MybatisPlusLanguageDriverAutoConfiguration.class})

这样启动就不会报错了。

其他步骤

上面这些开发完,就差不多可以实现数据库的动态切换从而实现读写分离了,不过其中有一个方法PropertyUtil,这是自定义的一个可以读取properties某个前缀下的所有属性的一个工具类。代码如下:


import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertyResolver;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class PropertyUtil {
    private static int springBootVersion = 2;
    public static <T> T handle(final Environment environment,final String prefix,final Class<T> clazz){
        switch (springBootVersion){
            case 1:
                return (T) v1(environment,prefix);
            case 2:
                return (T) v2(environment,prefix,clazz);
            default:
                throw new RuntimeException("Unsupported Spring Boot version");

        }
    }

    public static Object v1(final Environment environment,final String prefix){
        try {
            Class<?> resolverClass = Class.forName("org.springframework.boot.bind.RelaxedPropertyResolver");
            Constructor<?> resolverConstructor = resolverClass.getDeclaredConstructor(PropertyResolver.class);
            Method getSubPropertiesMethod = resolverClass.getDeclaredMethod("getSubProperties", String.class);
            Object resolverObject = resolverConstructor.newInstance(environment);
            String prefixParam = prefix.endsWith(".") ? prefix : prefix + ".";
            return getSubPropertiesMethod.invoke(resolverObject, prefixParam);
        } catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException
                       | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }

    }

    private static Object v2(final Environment environment, final String prefix, final Class<?> targetClass) {
        try {
            Class<?> binderClass = Class.forName("org.springframework.boot.context.properties.bind.Binder");
            Method getMethod = binderClass.getDeclaredMethod("get", Environment.class);
            Method bindMethod = binderClass.getDeclaredMethod("bind", String.class, Class.class);
            Object binderObject = getMethod.invoke(null, environment);
            String prefixParam = prefix.endsWith(".") ? prefix.substring(0, prefix.length() - 1) : prefix;
            Object bindResultObject = bindMethod.invoke(binderObject, prefixParam, targetClass);
            Method resultGetMethod = bindResultObject.getClass().getDeclaredMethod("get");
            return resultGetMethod.invoke(bindResultObject);
        }
        catch (final ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException
                     | IllegalArgumentException | InvocationTargetException ex) {
            throw new RuntimeException(ex.getMessage(), ex);
        }
    }
}

我将路由切换的功能逻辑单独拉成了一个SpringBootStarter,目录如下:
在这里插入图片描述
顺便介绍一下如何将以个项目在SpringBootStarter中自动装配
1.在resources中创建文件夹META-INF
2.创建spring.factories文件
3.在该文件中设置你需要自动装配的类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.xxx.wrdbrouter.config.DataSourceAutoConfig,\
  com.xxx.wrdbrouter.config.MyBatisPlusAutoConfig

就先记录这些,目前正在进行主从库同步的相关内容。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kafka学习-Java使用Kafka

文章目录 前言一、Kafka1、什么是消息队列offset 2、高性能topicpartition 3、高扩展broker 4、高可用replicas、leader、follower 5、持久化和过期策略6、消费者组7、Zookeeper8、架构图 二、安装Zookeeper三、安装Kafka四、Java中使用Kafka1、引入依赖2、生产者3、消费者4、运…

Unity使用sherpa-onnx实现离线语音合成

sherpa-onnx https://github.com/k2-fsa/sherpa-onnx 相关dll和lib库拷进Unity&#xff0c;官方示例代码稍作修改 using SherpaOnnx; using System; using System.IO; using System.Runtime.InteropServices; using UnityEngine;public class TTS : MonoBehaviour {public st…

Google I/O 2024 干货全解读:Gemini AI 横空出世,智能未来触手可及!

Google I/O 2024 干货全解读&#xff1a;Gemini AI 横空出世&#xff0c;智能未来触手可及&#xff01; 博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》…

git 拉取指定目录

指令方式 打开 git 自带的Git Bash 工具 以拉取github中 fastjson 的 /src/test/java/oracle/sql/ 目录为例 1.创建文件夹和git 初始化 cd D:/Program\ Files mkdir fastjson cd fastjson git init 2.设置允许克隆子目录 git config core.sparsecheckout true 3.添加远程…

前端开发攻略---用代码带你走近双色球再到远离双色球

1、演示 2、玩法及规则 双色球是一种流行的彩票游戏&#xff0c;它在很多国家都有自己的版本。以下是双色球的详细玩法&#xff1a; 选择号码&#xff1a;玩家需要从1至33的红色球中选择6个号码&#xff0c;并且从1至16的蓝色球中选择1个号码&#xff0c;构成一组7个号码。 购…

使用make_blobs生成数据并使用KNN机器学习算法进行分类和预测以及可视化

生成数据 使用make_blobs生成数据并使用matplotlib进行可视化 完整代码&#xff1a; from sklearn.datasets import make_blobs # KNN 分类器 from sklearn.neighbors import KNeighborsClassifier # 画图工具 import matplotlib.pyplot as plt # 数据集拆分工具 from sklea…

Linux 第三十一章

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C&#xff0c;linux &#x1f525;座右铭&#xff1a;“不要等到什么都没有了…

力扣127.单词接龙讲解

距离上一次刷题已经过去了.........嗯............我数一一下............整整十天&#xff0c;今天再来解一道算法题 由于这段时间准备简历&#xff0c;没咋写博客。。今天回来了&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&…

【二叉树】(二)二叉树的基础修改构造及属性求解1

&#xff08;二&#xff09;二叉树的基础修改构造及属性求解1 翻转二叉树递归实现迭代实现&#xff08;深度遍历&#xff09;层序实现&#xff08;广度遍历&#xff09; 对称二叉树递归实现迭代实现&#xff08;非层序遍历&#xff09; 二叉树的最大深度递归法迭代法&#xff0…

你了解黑龙江等保测评么?

等保测评的全称是信息安全等级保护测评&#xff0c;是信息安全等级保护工作中的一项重要内容。 具体来说&#xff0c;等保测评是指按照国家相关标准和技术规范&#xff0c;对信息系统安全等级保护状况进行检测评估的活动。 其主要目的是发现信息系统存在的安全隐患和不足&…

学会给文件夹加密,保密措施不可或缺!

我们的个人信息、工作文件和其他重要数据都存储在各种设备和文件夹中&#xff0c;如何保证这些数据的安全&#xff0c;防止未经授权的访问和泄露&#xff0c;成为了一个不容忽视的问题。本文将探讨给文件夹加密的必要性&#xff0c;以及如何在手机和电脑上进行文件夹加密。 操作…

使用KNN预测一个新的点,以及将这个点用五角星进行matplotlib可视化展示

概述 基于之前的KNN案例继续做一些操作。 之前的完整代码如下&#xff1a; from sklearn.datasets import make_blobs # KNN 分类器 from sklearn.neighbors import KNeighborsClassifier # 画图工具 import matplotlib.pyplot as plt # 数据集拆分工具 from sklearn.model_…

DiskGenius帮你恢复系统无法识别的U盘数据

场景还原 前两天早上U盘复制文件卡死后&#xff0c;强行断开U盘&#xff0c;再次使用直接无法访问&#xff0c;心拔凉拔凉&#xff01;&#xff01; 使用驱动器G:中的光盘之前需要将其格式化 位置不可用-无法访问U盘 常规科普 一、U盘无法识别 1、检查U盘是否插入正确&…

【汇编语言】多文件组织

【汇编语言】多文件组织 文章目录 【汇编语言】多文件组织前言一、8086拓展1.子程序的另外一种写法2.程序的多文件组织 总结 前言 本篇文章将讲到子程序的另一种写法&#xff0c;以及程序的多文件组织。 一、8086拓展 1.子程序的另外一种写法 初始的程序 在这里我们对比一下…

6款电脑精选工具软件推荐!

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/ 1.IP地址查看工具——纯真ip数据库 纯真IP数据库是一个易于操作的IP地址查询工具&#xff0c;它允许用户通过输入IP地址来查询其对应的地理位置…

每天五分钟玩转深度学习pytorch:pytorch中的张量类型

本文重点 和numpy一样,pytorch中也有自己的类型,本节课程我们将对它的类型进行介绍,并且学习不同的类型之间的转换,这是pytorch的基础。 基本类型 pytorch的基本变量称为张量Tensor,这张表是pytorch中的类型,Tensor有不同的类型,他和很多编程语言中的类型相似,它有 32…

ROS学习笔记(15)小车巡墙驾驶

0.前提 前一章我讲解了拉氏变换和PID&#xff0c;这一章我来讲解一下小车巡墙驾驶的理论和部分代码。 1.前情回顾 1.拉氏变换 拉普拉斯变换是要将时域问题转换成频域问题来处理。 2.PID控制器 转向角&#xff1a; 误差牺牲&#xff1a; 3.具体参看上一篇文章 2.巡墙驾驶…

AI 一键生成高清短视频,视频 UP 主们卷起来...

现在短视频越来越火&#xff0c;据统计&#xff0c;2023年全球短视频用户数量已达 10 亿&#xff0c;预计到2027年将突破 24 亿。对于产品展示和用户营销来说&#xff0c;短视频已经成为重要阵地&#xff0c;不管你喜不喜欢它&#xff0c;你都得面对它&#xff0c;学会使用它。…

Proxy和Reflect,打造灵活的JS代理机制 (代码示例)

在 JavaScript 中&#xff0c;代理&#xff08;Proxy&#xff09;和反射&#xff08;Reflect&#xff09;是 ES6 引入的两个新特性。Proxy用于创建一个对象的代理&#xff0c;从而实现对这个对象的操作的拦截、转换或扩展&#xff1b;而Reflect则提供了一系列与 JavaScript 运行…

线上3D博物馆搭建简单吗?有何优势?有哪些应用场景?

随着科技的飞速发展&#xff0c;传统的博物馆参观方式正在经历一场前所未有的变革&#xff0c;在科技的“加持”下&#xff0c;不少博物馆凭借强大的技术、创意和美学实践&#xff0c;频频“出圈”&#xff0c;线上3D博物馆逐渐崛起&#xff0c;这不仅丰富了人们的文化体验&…