自定义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下又有数据库操作,那么这个很容易导致切换数据源失败

实现效果

image-20240423165428947

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

  • 31
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
MyBatis-Plus 是一个在 MyBatis 基础上进行增强的持久层框架,提供了很多方便开发的功能和工具。关于 MyBatis-Plus 的读写分离,可以通过配置动态数据源和使用 MyBatis-Plus 提供的注解来实现读写分离是指将数据库的读操作和写操作分别在不同的数据库实例上进行,以提高系统的并发能力和性能。在 MyBatis-Plus 中,可以通过使用多个数据源实现读写分离。 首先,需要配置多个数据源,一个用于读操作,一个用于写操作。可以使用 Spring Boot 提供的配置方式,或者使用 MyBatis-Plus 提供的 DynamicDataSource 动态数据源。 然后,在需要进行读操作的方法上,可以使用 MyBatis-Plus 提供的 @Slave 注解,指定使用读数据源。例如: ```java @Slave public List<User> getUserList() { // ... } ``` 在需要进行写操作的方法上,则不需要特别指定数据源,默认会使用主数据源。 最后,在 MyBatis-Plus 的配置文件中,需要配置动态数据源的切换策略。可以通过使用 AbstractRoutingDataSource 类来实现切换策略,根据方法上的注解来决定使用哪个数据源。 这样配置之后,当调用带有 @Slave 注解的方法时,MyBatis-Plus 会自动切换到读数据源;调用其他方法时,会使用写数据源。从而实现读写分离的功能。 需要注意的是,读写分离的配置还涉及到数据库的主从同步和数据一致性等问题,在配置过程中需要综合考虑这些因素。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只牛博

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值