自定义mybatis插件实现读写分离

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 自定义mybatis插件实现读写分离


有时候我更想看到的是bug,比如做这个插件的时候

前言

在数据库的世界里,读写分离就像是一场神奇的变形术表演,能够让我们的应用程序更加稳定和高效。而MyBatis插件就像是一把神奇的魔杖,能够帮助我们实现数据库的读写分离。它就像是一位魔术师,能够在不同的数据库之间灵活切换,让我们的应用程序如虎添翼。现在,就让我们一起来揭开MyBatis插件的神秘面纱,探索它的魅力所在吧!

场景分析

要实现读写分离我们首先应该具备以下条件

1、多数据源场景,且可以动态切换数据源

2、在mybatis创建连接之前切换到想要的数据源

3、需要执行规则实现读写分离

大致就是上面的三点

前置配置讲解

# 数据源配置
spring.datasource.mysql.primary.url=jdbc:mysql://127.0.0.1:3361/base_sb?nullDatabaseMeansCurrent=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.mysql.primary.username=root
spring.datasource.mysql.primary.password=123456
spring.datasource.mysql.primary.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源配置
spring.datasource.mysql.slave1.url=jdbc:mysql://127.0.0.1:3351/dingding_mid?characterEncoding=utf8&serverTimezone=UTC
spring.datasource.mysql.slave1.username=root
spring.datasource.mysql.slave1.password=123456
spring.datasource.mysql.slave1.driver-class-name=com.mysql.cj.jdbc.Driver
@Bean(name = DataSourceType.PRIMARY)
@ConfigurationProperties(prefix = "spring.datasource.mysql.primary")
public DataSource primaryDataSource() {
  log.info("主数据库连接池创建中.......");
  return DruidDataSourceBuilder.create().build();
}
@Bean(name = DataSourceType.SECOND)
@ConfigurationProperties(prefix = "spring.datasource.mysql.slave1")
public DataSource secondDataSource() {
  log.info("second数据库连接池创建中.......");
  return DruidDataSourceBuilder.create().build();
}

上面是我的数据源,需要实现的就是当进行查询的时候我会走到slave1。

注意:我这里仅仅是为了展示效果,真正的读写分离是读库和写库一模一样,唯一的区别是读库read_only=1,也就是只读状态,并且他们的关系还是主从关系。

数据源切换实现

springboot整合多数据源的配置以及动态切换数据源,注解切换数据源

代码实现(插件)

package com.todoitbo.baseSpringbootDasmart.interceptor;
import com.todoitbo.baseSpringbootDasmart.multiDataSource.DataSourceContextHolder;
import com.todoitbo.baseSpringbootDasmart.multiDataSource.DataSourceType;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.sql.Connection;
import java.util.Properties;
/**
 * @author xiaobo
 */
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Component
public class RoutingInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 使用MetaObject获取MappedStatement
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        while (metaObject.hasGetter("h")) {
            Object object = metaObject.getValue("h");
            metaObject = SystemMetaObject.forObject(object);
        }
        while (metaObject.hasGetter("target")) {
            Object object = metaObject.getValue("target");
            metaObject = SystemMetaObject.forObject(object);
        }
        // 通过反射获取到当前MappedStatement高版本没这个类了
        // MappedStatement mappedStatement = (MappedStatement) MetaObjectUtils.getFieldValue(statementHandler, "delegate.mappedStatement");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String sqlCommandType = mappedStatement.getSqlCommandType().toString();
        
        // 根据SQL命令类型,动态切换数据源
        if ("SELECT".equals(sqlCommandType)) {
            // 设置为数据库1的连接
            DataSourceContextHolder.setDataSource(DataSourceType.SECOND);
        } else {
            // 设置为数据库2的连接
            DataSourceContextHolder.setDataSource(DataSourceType.PRIMARY);
        }
        // 继续执行原有逻辑
        return invocation.proceed();
    }
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否则直接返回
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
    @Override
    public void setProperties(Properties properties) {
        // 这里可以接收到配置文件中的属性
    }
}

说明

当然,让我们逐一解析 RoutingInterceptor 类的主要方法:

  1. intercept(Invocation invocation): 这是拦截器的核心方法,当被拦截的方法(在本例中是 StatementHandlerprepare 方法)被调用时,这个方法会被执行。
    在这个方法中,首先获取了 StatementHandler 对象,然后通过 MetaObject 获取了 MappedStatement 对象。根据 MappedStatement 中的 sqlCommandType 判断当前执行的 SQL 是查询还是非查询,然后用 DataSourceContextHolder.setDataSource() 方法动态设置数据源,最后调用 invocation.proceed() 继续执行原有逻辑。
  2. plugin(Object target): 这个方法用于包装目标对象。当目标对象是 StatementHandler 类型时,使用 Plugin.wrap(target, this) 方法包装目标对象,这样当目标对象的方法被调用时,会先调用 intercept 方法。如果目标对象不是 StatementHandler 类型,直接返回目标对象。
  3. setProperties(Properties properties): 这个方法可以用于从配置文件中接收属性,但在这个拦截器中并未使用。

以上就是 RoutingInterceptor 类的主要方法。这个类实现了 MyBatis 的 Interceptor 接口,通过 @Intercepts@Signature 注解指定了要拦截的方法,然后在 intercept 方法中实现了动态数据源路由的逻辑。

注意

重点提一下需要注意的点

如果你除了这个拦截插件用到切换数据源之外还有别的,比如上面提到的数据源的切换,你定义了一个AOP,这个切点是service上,而你的这个service下又有数据库操作,那么这个很容易导致切换数据源失败

实现效果

因为我的AOP干扰整整解决了1个多小时,弱弱的说自己一句好菜。终于等到了这个异常,也就是我的查询走的表是另一个库的表,而这个库并没有这个表。大公告成,完美收工

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
4月前
|
SQL XML Java
8、Mybatis-Plus 分页插件、自定义分页
这篇文章介绍了Mybatis-Plus的分页功能,包括如何配置分页插件、使用Mybatis-Plus提供的Page对象进行分页查询,以及如何在XML中自定义分页SQL。文章通过具体的代码示例和测试结果,展示了分页插件的使用和自定义分页的方法。
8、Mybatis-Plus 分页插件、自定义分页
|
4月前
|
SQL Java 测试技术
3、Mybatis-Plus 自定义sql语句
这篇文章介绍了如何在Mybatis-Plus框架中使用自定义SQL语句进行数据库操作。内容包括文档结构、编写mapper文件、mapper.xml文件的解释说明、在mapper接口中定义方法、在mapper.xml文件中实现接口方法的SQL语句,以及如何在单元测试中测试自定义的SQL语句,并展示了测试结果。
3、Mybatis-Plus 自定义sql语句
|
1月前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
201 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
4月前
|
SQL Java Kotlin
MybatisPlus怎么拓展自定义BaseMapper
通过扩展Mybatis-Plus的`BaseMapper`,可以自定义SQL模板以满足特定业务需求。例如,当遇到唯一键冲突而不希望抛出异常时,可使用`INSERT IGNORE`语法。首先,创建`InsertIgnore`类继承`AbstractMethod`并定义`insertIgnore`方法及其SQL模板。接着,在自定义的`UltraBaseMapper`接口中声明`insertIgnore`方法,并让业务Mapper继承此接口。最后,通过`UltraSqlInjector`类将`InsertIgnore`方法注册到Mybatis-Plus插件中。
186 1
|
1月前
|
SQL Java 数据库连接
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
MyBatis-Plus是一个MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。本文讲解了最新版MP的使用教程,包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段等核心功能。
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
|
1月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
3月前
|
SQL Java 数据库连接
解决mybatis-plus 拦截器不生效--分页插件不生效
本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
965 6
解决mybatis-plus 拦截器不生效--分页插件不生效
|
3月前
|
SQL XML Java
springboot整合mybatis-plus及mybatis-plus分页插件的使用
这篇文章介绍了如何在Spring Boot项目中整合MyBatis-Plus及其分页插件,包括依赖引入、配置文件编写、SQL表创建、Mapper层、Service层、Controller层的创建,以及分页插件的使用和数据展示HTML页面的编写。
springboot整合mybatis-plus及mybatis-plus分页插件的使用
|
4月前
|
Java 数据库 Spring
MyBatisPlus分页插件在SpringBoot中的使用
这篇文章介绍了如何在Spring Boot项目中配置和使用MyBatis-Plus的分页插件,包括创建配置类以注册分页拦截器,编写测试类来演示如何进行分页查询,并展示了测试结果和数据库表结构。
MyBatisPlus分页插件在SpringBoot中的使用
|
5月前
|
SQL Java 数据库连接
idea中配置mybatis 映射文件模版及 mybatis plus 自定义sql
idea中配置mybatis 映射文件模版及 mybatis plus 自定义sql
104 3
下一篇
DataWorks