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

news2024/9/23 15:29:48

读写分离有必要吗?

实现读写分离势必要与你所做的项目相关,如果项目读多写少,那就可以设置读写分离,让“读”可以更快,因为你可以把你的“读”数据库的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

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

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

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

相关文章

python turtle 升国旗

​一、导语 大家好,前段时间,我们画出了五星红旗,今天我们要用Python的Turtle库来绘制一个五星红旗,并让国旗上升,让我们一起来感受编程与艺术的完美结合吧!领略国家的强大!爱祖国,做一个遵纪守法的好公民。 二、效果展示 升国旗 三、开发过程 一、准备工作 首先我们…

品牌银饰售卖|基于SSM+vue的品牌银饰售卖平台的设计与实现(源码+数据库+文档)

品牌银饰售卖平台 目录 基于SSM&#xff0b;vue的品牌银饰售卖平台的设计与实现 一、前言 二、系统设计 三、系统功能设计 1前台功能模块 2后台功能模块 5.2.1管理员功能模块 5.2.2用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题…

怎么扫码查看文件内容?多文件一键生成二维码的方法

现在日常生活中经常会看到很多的二维码中包含文件&#xff0c;扫码后在手机上预览文件内容或者下载文件&#xff0c;有很多的应用场景下被使用。通过扫描二维码的方式实现文件的传递&#xff0c;与传统方式相比更加方便快捷。 这种方式能够提升获取文件的便捷性&#xff0c;而…

在大型项目上,Python 是个烂语言吗?

在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「Python的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01; python项目超过5万行&#x…

谷粒商城实战(024 业务-订单模块-分布式事务1)

Java项目《谷粒商城》架构师级Java项目实战&#xff0c;对标阿里P6-P7&#xff0c;全网最强 总时长 104:45:00 共408P 此文章包含第284p-第p290的内容 简介 模拟积分服务出异常&#xff0c;前方的锁库存事务未回滚&#xff0c;这时候就需要分布式事务 本地事务 事务的隔离…

echarts 环形图实现透明间隔,嵌套环形图片和图形

echarts 环形图实现透明间隔&#xff0c;嵌套环形图片和图形 环形图实现透明间隔环形图嵌套环形图片环形图嵌套环形图形 环形图实现透明间隔 首先通过 radius 属性实现一个圆环图 再通过 padAngle 属性设置扇区角度即可 使用 borderRadius 属性设置扇形区块的内外圆角半径&…

新时代高速数据中心800G DR8光模块解决方案

近年来&#xff0c;随着5G网络、存储介质和计算能力等基础技术的不断升级&#xff0c;100G和400G数据中心得到了普及。如今800G数据中心时代也已经来临。本文将围绕800G DR8来介绍飞速&#xff08;FS&#xff09;800G数据中心解决方案&#xff0c;旨在为全球客户提供全面且高性…

如何在阿里云申请免费SSL证书(三个月有效)

SSL证书主要用于建立Web服务器和客户端间可信的HTTPS协议加密链接&#xff0c;以防止数据在传输过程中被篡改&#xff0c;避免信息泄露。阿里云提供了多种品牌和类型的SSL证书&#xff0c;以满足不同用户的需求。您可以根据自己的预算、域名类型以及网站类型&#xff0c;选择购…

【Qt】按钮类控件(二)

文章目录 按钮类控件1、Push Button代码示例: 带有图标的按钮代码示例: 带有快捷键的按钮 2、Radio Buttion代码示例: click, press, release, toggled 的区别代码示例: 单选框分组&#xff08;QButtonGroup&#xff09; 3、 Check Box代码示例: 获取复选按钮的取值 按钮类控件…

Cow Exhibition G的来龙去脉

[USACO03FALL] Cow Exhibition G - 洛谷 曲折经过 爆搜 一开始没什么好的想法&#xff0c;就针对每头奶牛去or不去进行了爆搜。 #include <cstdio> #include <algorithm> using namespace std;#define maxn 405 int iq[maxn], eq[maxn]; int ans; int n;void d…

C++系统编程篇——Linux初识(系统安装、权限管理,权限设置)

(1)linux系统的安装 双系统---不推荐虚拟机centos镜像&#xff08;可以使用&#xff09;云服务器/轻量级云服务器&#xff08;强烈推荐&#xff09; ①云服务器&#xff08;用xshell连接&#xff09; ssh root公网IP 然后输入password ①添加用户&#xff1a; addus…

企业研发必备网络:这些关键特性,你get了吗?

对于以研发为核心的企业&#xff0c;如软件开发、生物制药、智能汽车等&#xff0c;安全、稳定的研发网络可是他们业务发展不可或缺的。那么&#xff0c;这些研发网络究竟有哪些独特之处&#xff0c;又能为企业带来哪些价值呢&#xff1f; 首先&#xff0c;我们知道企业研发常常…

【设计模式】JAVA Design Patterns——Adapter(适配器模式)

&#x1f50d;目的 将一个接口转换成另一个客户所期望的接口。适配器让那些本来因为接口不兼容的类可以合作无间。 &#x1f50d;解释 现实世界例子 考虑有这么一种情况&#xff0c;在你的存储卡中有一些照片&#xff0c;你想将其传到你的电脑中。为了传送数据&#xff0c;你需…

【管理咨询宝藏104】普华永道财务管理与内控培训

本报告首发于公号“管理咨询宝藏”&#xff0c;如需阅读完整版报告内容&#xff0c;请查阅公号“管理咨询宝藏”。 【管理咨询宝藏104】普华永道财务管理与内控培训 【格式】PDF版本 【关键词】普华永道、四大、财务管理 【核心观点】 - 职能转变后&#xff0c;财务在决策支持…

「前端」性能优化问题总结

前言 本文主要介绍一些前端通用的性能优化方案总结&#xff0c;非写代码阶段的性能优化。 分包 React router V6.4 数据路由新特性 <Route path/xx lazy{async()>{const module await import(./xx)const XX module.defaultreturn{element:(<Suspense fallback…

紫光展锐先进技术科普 | 工业互联网遇到5G,1+1>2?

随着工厂自动化的加速普及&#xff0c;如今我们可能经常看到这样的场景&#xff1a;在高温、潮湿、粉尘、腐蚀等恶劣环境作业场景&#xff0c;巡检机器人穿梭其中&#xff0c;工人们不必弯腰去搬沉重又危险的器件&#xff0c;而旁边会有一个个机械臂帮手平稳有序地完成好所有搬…

HTTP客户端手动解析响应体数据

服务端 package mainimport ("easyGo/person""encoding/json""net/http" )func main() {http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {p : &person.Person{Name: "jackie",Age: 30,T: pe…

【JAVA进阶篇教学】第十六篇:Java中AOP使用

博主打算从0-1讲解下java进阶篇教学&#xff0c;今天教学第十五篇&#xff1a;Java中AOP使用。 AOP&#xff08;Aspect-Oriented Programming&#xff09;是一种编程范式&#xff0c;它允许开发者在不修改源代码的情况下&#xff0c;对代码进行横切关注点的分离和增强。在 Java…

Linux上安装python指南

公司的linux服务器上只有自带的python2,折腾了一下安装python3,后来在网上搜发现装miniconda会更加方便。 1、 下载miniconda安装包 清华镜像下载&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/ 点这里下载 2、 上传Linux安装 #安装在/usr/local/mini…

Spring AI默认gpt版本源码探究

Spring AI默认gpt版本源码探究 调试代码 通过调试&#xff0c;可以看到默认mdel为gpt-3.5-turbo 源码探究 进入OpenAiChatClient类查看具体的代码信息 可以看到如下代码&#xff0c;在有参构造方法中可以看到&#xff0c;model默认使用OpenAiApi.DEFAULT_CHAT_MODELpublic…