自定义分库分表组件(实现分库分表的组件)——java

news2024/11/18 7:45:00

系列文章目录

文章目录

  • 系列文章目录
  • 前言
  • 一、所需技术
  • 二、技术总结
    • 1. ThreadLocal
    • 2.HashMap
  • 三、实现
  • 1、定义路由注解
    • 自定义注解
      • 格式
      • 要求
    • 元注解: 用于描述注解的注解
    • 在程序使用(解析)注解:获取注解中定义的属性值
    • 小例子:注解定义一个简单的测试框架
    • 实战写法
    • 小总结
  • 2、解析路由配置
  • 3. 数据源切换
    • 两个注解的作用
    • 数据源的创建
  • 4. 切面拦截
    • 案例
    • 分析结果
  • 5. Mybatis 拦截器处理分表
  • 最后


前言

可以先看下上文的基础知识——为什么分库分表

本文是借鉴小傅哥的笔记 整理的自己学习笔记 仅作学习使用 如有侵权请联系

本文是对分库分表组件的具体实现:我们要实现的也是水平拆分的路由设计,如图
在这里插入图片描述

一、所需技术

  • 是关于 AOP 切面拦截的使用,这是因为需要给使用数据库路由的方法做上标记,便于处理分库分表逻辑。
  • 数据源的切换操作,既然有分库那么就会涉及在多个数据源间进行链接切换,以便把数据分配给不同的数据库。
  • 数据库表寻址操作,一条数据分配到哪个数据库,哪张表,都需要进行索引计算。在方法调用的过程中最终通过 ThreadLocal 记录。
  • 为了能让数据均匀的分配到不同的库表中去,还需要考虑如何进行数据散列的操作,不能分库分表后,让数据都集中在某个库的某个表,这样就失去了分库分表的意义。
    综上,可以看到在数据库和表的数据结构下完成数据存放,我需要用到的技术包括:AOP、数据源切换、散列算法、哈希寻址、ThreadLocal以及SpringBoot的Starter开发方式等技术。而像哈希散列、寻址、数据存放,其实这样的技术与 HashMap 有太多相似之处

二、技术总结

1. ThreadLocal

在这里插入图片描述

@Test
public void test_idx() {
    int hashCode = 0;
    for (int i = 0; i < 16; i++) {
        hashCode = i * 0x61c88647 + 0x61c88647;
        int idx = hashCode & 15;
        System.out.println("斐波那契散列:" + idx + " 普通散列:" + (String.valueOf(i).hashCode() & 15));
    }
} 

斐波那契散列:7 普通散列:0
斐波那契散列:14 普通散列:1
斐波那契散列:5 普通散列:2
斐波那契散列:12 普通散列:3
斐波那契散列:3 普通散列:4
斐波那契散列:10 普通散列:5
斐波那契散列:1 普通散列:6
斐波那契散列:8 普通散列:7
斐波那契散列:15 普通散列:8
斐波那契散列:6 普通散列:9
斐波那契散列:13 普通散列:15
斐波那契散列:4 普通散列:0
斐波那契散列:11 普通散列:1
斐波那契散列:2 普通散列:2
斐波那契散列:9 普通散列:3
斐波那契散列:0 普通散列:4

2.HashMap

在这里插入图片描述

public static int disturbHashIdx(String key, int size) {
    return (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
}

三、实现

1、定义路由注解

自定义注解

  • 概念:说明程序的。给计算机看的
    注释:用文字描述程序的。给程序员看的
    定义:注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。概念描述:JDK1.5之后的新特性 说明程序的
    使用注解:@注解名称

格式

元注解 public @interface 注解名称{ 属性列表; }

注解本质上就是一个接口,该接口默认继承Annotation接口

public interface MyAnno extends java.lang.annotation.Annotation {}
接口中可能有抽象方法

要求

1、属性的返回值类型有下列取值:基本数据类型、String、枚举、注解以上类型的数组
2、定义了属性,在使用时需要给属性赋值
3、如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
4、如果只有一个属性需要赋值,并且属性的名称是value,则value可以省略,直接定义值即可。
数组赋值时,值使用{}包裹。如果数组中只有一个值,则{}可以省略

定义:
public @interface MyAnno {
    int value();
    Person per();
    MyAnno2 anno2();
    String[] strs();
}

public enum Person {

    P1,P2;
}

使用:
@MyAnno(value=12,per = Person.P1,anno2 = @MyAnno2,strs="bbb")
public class Worker {

}

元注解: 用于描述注解的注解

@Target:描述注解能够作用的位置
ElementType取值:
TYPE:可以作用于类上
METHOD:可以作用于方法上
FIELD:可以作用于成员变量上
@Retention:描述注解被保留的阶段
@Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到,自定义注解一般用这个。
@Documented:描述注解是否被抽取到api文档中
@Inherited:描述注解是否被子类继承

在程序使用(解析)注解:获取注解中定义的属性值

之前反射的范例

/**
前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
*/

//1.加载配置文件
//1.1创建Properties对象
Properties pro = new Properties();
//1.2加载配置文件,转换为一个集合
//1.2.1获取class目录下的配置文件
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
pro.load(is);

//2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");


//3.加载该类进内存
Class cls = Class.forName(className);
//4.创建对象
Object obj = cls.newInstance();
//5.获取方法对象
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj);

在反射中有通过读取配置文件来创建任意类的对象,执行任意方法。
我们可以通过注解替换上述读取配置文件相关操作。具体代码如下: 注解定义如下:

/**
 * 描述需要执行的类名,和方法名
 * @author ymj
 */

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pro {

    String className();
    String methodName();
}

通过解析注解配置,执行相关对象创建和执行对象方法。

  • 获取注解定义的位置的对象 (Class,Method,Field)
  • 获取指定的注解
  • 调用注解中的抽象方法获取配置的属性值
    代码如下:
@Pro(className = "com.zjq.javabase.base25.annotation.Demo1",methodName = "show")
public class ReflectTest {
    public static void main(String[] args) throws Exception {

        /**
         * 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
         */

        //1.解析注解
        //1.1获取该类的字节码文件对象
        Class<ReflectTest> reflectTestClass = ReflectTest.class;
        //2.获取上边的注解对象
        //其实就是在内存中生成了一个该注解接口的子类实现对象
        /*

            public class ProImpl implements Pro{
                public String className(){
                    return "com.zjq.javabase.base25.annotation.Demo1";
                }
                public String methodName(){
                    return "show";
                }

            }
         */
        Pro an = reflectTestClass.getAnnotation(Pro.class);
        //3.调用注解对象中定义的抽象方法,获取返回值
        String className = an.className();
        String methodName = an.methodName();
        System.out.println(className);
        System.out.println(methodName);


        //4.加载该类进内存
        Class cls = Class.forName(className);
        //5.创建对象
        Object obj = cls.newInstance();
        //6.获取方法对象
        Method method = cls.getMethod(methodName);
        //7.执行方法
        method.invoke(obj);
    }
}

小例子:注解定义一个简单的测试框架

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {
}

定义一个计算器工具类,并在方法上使用@Check注解

/**
 * 定义的计算器类
 * @author ymj
 */
public class Calculator {

    //加法
    @Check
    public void add(){
        String str = null;
        str.toString();
        System.out.println("1 + 0 =" + (1 + 0));
    }
    //减法
    @Check
    public void sub(){
        System.out.println("1 - 0 =" + (1 - 0));
    }
    //乘法
    @Check
    public void mul(){
        System.out.println("1 * 0 =" + (1 * 0));
    }
    //除法
    @Check
    public void div(){
        System.out.println("1 / 0 =" + (1 / 0));
    }

    public void show(){
        System.out.println("永无bug...");
    }

}

定义测试框架类并执行测试,把测试异常记录到bug.txt文件中,代码如下:

/**
 * 简单的测试框架
 * 当主方法执行后,会自动自行被检测的所有方法(加了Check注解的方法),判断方法是否有异常,
 * 记录到文件中
 *
 * @author ymj
 */
public class TestCheck {

    public static void main(String[] args) throws IOException {
        //1.创建计算器对象
        Calculator c = new Calculator();
        //2.获取字节码文件对象
        Class cls = c.getClass();
        //3.获取所有方法
        Method[] methods = cls.getMethods();

        int number = 0;//出现异常的次数
        BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));


        for (Method method : methods) {
            //4.判断方法上是否有Check注解
            if (method.isAnnotationPresent(Check.class)) {
                //5.有,执行
                try {
                    method.invoke(c);
                } catch (Exception e) {
                    //6.捕获异常

                    //记录到文件中
                    number++;

                    bw.write(method.getName() + " 方法出异常了");
                    bw.newLine();
                    bw.write("异常的名称:" + e.getCause().getClass().getSimpleName());
                    bw.newLine();
                    bw.write("异常的原因:" + e.getCause().getMessage());
                    bw.newLine();
                    bw.write("--------------------------");
                    bw.newLine();

                }
            }
        }

        bw.write("本次测试一共出现 " + number + " 次异常");

        bw.flush();
        bw.close();

    }

}

执行测试后可以在src同级目录查看到bug.txt文件内容如下:

add 方法出异常了
异常的名称:NullPointerException
异常的原因:null
div 方法出异常了
异常的名称:ArithmeticException
异常的原因:/ by zero 

实战写法

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {

    String key() default "";

}

小总结

大多数时候,我们只是使用注解,而不是自定义注解。
注解给谁用?

  • 编译器
  • 给解析程序用
    注解不是程序的一部分,可以理解为注解就是一个标签。
@Mapper
public interface IUserDao {

     @DBRouter(key = "userId")
     User queryUserInfoByUserId(User req);

     @DBRouter(key = "userId")
     void insertUser(User req);

}

  • 首先我们需要自定义一个注解,用于放置在需要被数据库路由的方法上。
  • 它的使用方式是通过方法配置注解,就可以被我们指定的 AOP 切面进行拦截,拦截后进行相应的数据库路由计算和判断,并切换到相应的操作数据源上。

2、解析路由配置

这篇文章也写的不错
点这里!!!!!

路由配置的话 需要设置分库分表的 要在 自己的application.yml中定义多数据源配置

配置三个库的信息

  • 以上就是我们实现完数据库路由组件后的一个数据源配置,在分库分表下的数据源使用中,都需要支持多数据源的信息配置,这样才能满足不同需求的扩展。
  • 对于这种自定义较大的信息配置,就需要使用到 org.springframework.context.EnvironmentAware 接口,来获取配置文件并提取需要的配置信息。

获取配置的话 需要实现上面这个接口 然后重写setEnvironment的方法

咱们这里介绍凡是被spring管理的类,实现接口 EnvironmentAware 重写方法 setEnvironment 可以在工程启动时,获取到系统环境变量和application配置文件中的变量。
范例:

package  com.kfit.environment;
  
import  org.springframework.beans.factory.annotation.Value;
import  org.springframework.boot.bind.RelaxedPropertyResolver;
import  org.springframework.context.EnvironmentAware;
import  org.springframework.context.annotation.Configuration;
import  org.springframework.core.env.Environment;
  
/**
  * 主要是@Configuration,实现接口:EnvironmentAware就能获取到系统环境信息;
  *
  * 
  */
@Configuration
public  class  MyEnvironmentAware  implements  EnvironmentAware{
  
        //注入application.properties的属性到指定变量中.
        @Value ( "${spring.datasource.url}" )
        private  String myUrl;
       
        /**
         *注意重写的方法 setEnvironment 是在系统启动的时候被执行。
         */
        @Override
        public  void  setEnvironment(Environment environment) {
              
               //打印注入的属性信息.
               System.out.println( "myUrl=" +myUrl);
              
               //通过 environment 获取到系统属性.
               System.out.println(environment.getProperty( "JAVA_HOME" ));
              
               //通过 environment 同样能获取到application.properties配置的属性.
               System.out.println(environment.getProperty( "spring.datasource.url" ));
              
               //获取到前缀是"spring.datasource." 的属性列表值.
               RelaxedPropertyResolver relaxedPropertyResolver =  new  RelaxedPropertyResolver(environment,  "spring.datasource." );
               System.out.println( "spring.datasource.url=" +relaxedPropertyResolver.getProperty( "url" ));
        System.out.println( "spring.datasource.driverClassName=" +relaxedPropertyResolver.getProperty( "driverClassName" ));
        }
}

其中application.properties文件信息是:

 ########################################################
###datasource
########################################################
spring.datasource.url = jdbc:mysql: //localhost:3306/test
spring.datasource.username = root
spring.datasource.password = root
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.max-active= 20
spring.datasource.max-idle= 8
spring.datasource.min-idle= 8
spring.datasource.initial-size= 10
@Override
public void setEnvironment(Environment environment) {
    String prefix = "router.jdbc.datasource.";    
	//prefix,是数据源配置的开头信息,你可以自定义需要的开头内容。
//dbCount 分库数量、tbCount 分表数量、dataSources 数据源、dataSourceProps ,
//都是对配置信息的提取,并存放到 dataSourceMap (数据源配置组)中便于后续使用。
    dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
    tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));    

    String dataSources = environment.getProperty(prefix + "list");
    for (String dbInfo : dataSources.split(",")) {
        Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
        dataSourceMap.put(dbInfo, dataSourceProps);
    }
}

当然 这里面的PropertyUtil是自己定义的读取配置文件操作工具类 工具类中通过反射的原理 handle 函数(根据springboot version的版本)跳转到自己的v1和v2方法 类似上面的自定义注解的配置解析

3. 数据源切换

在结合 SpringBoot 开发的 Starter 中,需要提供一个 DataSource 的实例化对象,那么这个对象我们就放在 DataSourceAutoConfig 来实现,并且这里提供的数据源是可以动态变换的,也就是支持动态切换数据源。
这里说明一下

两个注解的作用

Spring Boot 推荐使用 java 配置完全代替 XML 配置,java 配置是通过 @Configration 和 @Bean 注解实现的。二者作用如下:

  • @Configration 注解:声明当前类是一个配置类,相当于 Spring 中的一个 XML 文件
  • @Bean 注解:作用在方法上,声明当前方法的返回值是一个 Bean
    不懂的话 详情请点击 这里!!!!!!!

同时也要把@Component 和 @Bean 的区别 看好

数据源的创建

@Bean
public DataSource dataSource() {
    // 创建数据源
    Map<Object, Object> targetDataSources = new HashMap<>();
    for (String dbInfo : dataSourceMap.keySet()) {
        Map<String, Object> objMap = dataSourceMap.get(dbInfo);
        //new 了一个构造器
        targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
    }     

    // 设置数据源
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    //targetDataSources:保存多个数据源的map
//defaultTargetDataSource:指默认的数据源
    dynamicDataSource.setTargetDataSources(targetDataSources);
    dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(), defaultDataSourceConfig.get("username").toString(), defaultDataSourceConfig.get("password").toString()));

    return dynamicDataSource;
}

这里是一个简化的创建案例,把基于从配置信息中读取到的数据源信息,进行实例化创建。
由于是多库 不止一个 所以用DynamicDataSource
而这个库DriverManagerDataSource
只是连接数据库的一种方式

数据源创建完成后存放到 DynamicDataSource 中,DynamicDataSource这个类 在本文中是自定义的一个类 它是一个继承了 AbstractRoutingDataSource 的实现类,这个类里可以存放和读取相应的具体调用的数据源信息。
参考文章:这里!!!!
targetDataSources:保存多个数据源的map
defaultTargetDataSource:指默认的数据源
下面的是好文章 首先targetDataSources是一个map,根据key保存不同的数据源,源码里面看到targetDataSources会转换成另一个map的变量resolvedDataSources,而defaultTargetDataSource转换成resolvedDefaultDataSource

springboot动态多数据源配置和使用(二)

4. 切面拦截

在 AOP 的切面拦截中需要完成;数据库路由计算、扰动函数加强散列、计算库表索引、设置到 ThreadLocal 传递数据源,整体案例代码如下:
这个开头切面的代码是加入了一个Pointcut这个切入点的集合

Pointcut里面加入@annotation:用于匹配当前执行方法持有指定注解的方法

@annotation(注解类型):匹配被调用的方法上有指定的注解。

案例

定义一个注解,可以用在方法上

package com.javacode2018.aop.demo9.test12;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Ann12 {
}

定义2个类

S12Parent为父类,内部定义了2个方法,2个方法上都有@Ann12注解
S12是代理的目标类,也是S12Parent的子类,内部重写了m2方法,重写之后m2方法上并没有@Ann12注解,S12内部还定义2个方法m3和m4,而m3上面有注解@Ann12
package com.javacode2018.aop.demo9.test12;

class S12Parent {

    @Ann12
    public void m1() {
        System.out.println("我是S12Parent.m1()方法");
    }

    @Ann12
    public void m2() {
        System.out.println("我是S12Parent.m2()方法");
    }
}

public class S12 extends S12Parent {

    @Override
    public void m2() {
        System.out.println("我是S12.m2()方法");
    }

    @Ann12
    public void m3() {
        System.out.println("我是S12.m3()方法");
    }

    public void m4() {
        System.out.println("我是S12.m4()方法");
    }
}

来个Aspect类

当被调用的目标方法上有@Ann12注解的时,会被beforeAdvice处理。
package com.javacode2018.aop.demo9.test12;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectTest12 {

    @Pointcut("@annotation(com.javacode2018.aop.demo9.test12.Ann12)")
    public void pc() {
    }

    @Before("pc()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println(joinPoint);
    }
}

测试用例

S12作为目标对象,创建代理,然后分别调用4个方法
@Test
public void test12() {
    S12 target = new S12();
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.addAspect(AspectTest12.class);
    S12 proxy = proxyFactory.getProxy();
    proxy.m1();
    proxy.m2();
    proxy.m3();
    proxy.m4();
}

运行输出

execution(void com.javacode2018.aop.demo9.test12.S12Parent.m1())
我是S12Parent.m1()方法
我是S12.m2()方法
execution(void com.javacode2018.aop.demo9.test12.S12.m3())
我是S12.m3()方法
我是S12.m4()方法

分析结果

m1方法位于S12Parent中,上面有@Ann12注解,被连接了,m3方法上有@Ann12注解,被拦截了,而m4上没有@Ann12注解,没有被拦截,这3个方法的执行结果都很容易理解。

重点在于m2方法的执行结果,没有被拦截,m2方法虽然在S12Parent中定义的时候也有@Ann12注解标注,但是这个方法被S1给重写了,在S1中定义的时候并没有@Ann12注解,代码中实际上调用的是S1中的m2方法,发现这个方法上并没有@Ann12注解,所以没有被拦截。
针对这个切入点的集合用法详情 点这里!!!!

@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
    String dbKey = dbRouter.key();
    //StringUtils类与String类的区别在于:此类是null安全的,
    //即如果输入参数String为null,则不会抛出NullPointerException异常,代码更健壮。
    if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");

    // 计算路由
    String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
    int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();

    // 扰动函数
    int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));

    // 库表索引
    int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
    int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);   

    // 设置到 ThreadLocal
    DBContextHolder.setDBKey(String.format("%02d", dbIdx));
    DBContextHolder.setTBKey(String.format("%02d", tbIdx));
    logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
   
    // 返回结果
    try {
        return jp.proceed();
    } finally {
        DBContextHolder.clearDBKey();
        DBContextHolder.clearTBKey();
    }
}

DBContextHolder这个自己定义是数据源是上下文 里面是两个ThreaLocal类型的dbKeytbKey
定义了set和get方法 还有clearDBKey()的方法 里面的dbKey.remove();

  • 简化的核心逻辑实现代码如上,首先我们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用。
  • 接下来使用和 HashMap 一样的扰动函数逻辑,让数据分散的更加散列。
  • 当计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表。
  • 最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息。

5. Mybatis 拦截器处理分表

这块内容属于Mybatis源码系列的内容了 mybatis:基于mybatis拦截器分表实现。

  • 最开始考虑直接在Mybatis对应的表 INSERT INTO user_strategy_export_${tbIdx} 添加字段的方式处理分表。但这样看上去并不优雅,不过也并不排除这种使用方式,仍然是可以使用的。
  • 那么我们可以基于 Mybatis 拦截器进行处理,通过拦截 SQL 语句动态修改添加分表信息,再设置回 Mybatis 执行 SQL 中。
  • 此外再完善一些分库分表路由的操作,比如配置默认的分库分表字段以及单字段入参时默认取此字段作为路由字段。
  • Java中Pattern.compile函数的用法
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DynamicMybatisPlugin implements Interceptor {


    private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取StatementHandler
        //先拦截到RoutingStatementHandler,
        //里面有个StatementHandler类型的delegate变量,
        //其实现类是BaseStatementHandler,然后就到BaseStatementHandler的成员变量mappedStatement
 
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new DefaultReflectorFactory());
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 获取自定义注解判断是否进行分表操作
        String id = mappedStatement.getId();
        String className = id.substring(0, id.lastIndexOf("."));
        Class<?> clazz = Class.forName(className);
        DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
        if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
          // 传递给下一个拦截器处理
            return invocation.proceed();
        }

        // 获取SQL
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();

        // 替换SQL表名 USER 为 USER_03
        Matcher matcher = pattern.matcher(sql);
        String tableName = null;
        if (matcher.find()) {
            tableName = matcher.group().trim();
        }
        assert null != tableName;
        String replaceSql = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());

        // 通过反射修改SQL语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, replaceSql);

        return invocation.proceed();
    }

}

在这里插入图片描述

  • 实现 Interceptor 接口的 intercept 方法,获取StatementHandler、通过自定义注解判断是否进行分表操作、获取SQL并替换SQL表名 USER 为 USER_03、最后通过反射修改SQL语句
  • 此处会用到正则表达式拦截出匹配的sql,(from|into|update)[\s]{1,}(\w{1,})

最后

接下来就是验证环节了 验证分库与分表

  • 打包 db-router-spring-boot-starter
  • 引入 pom 文件
<dependency>
    <groupId>cn.bugstack.middleware</groupId>
    <artifactId>db-router-spring-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
  1. 在需要使用数据库路由的DAO方法上加入注解
    cn.itedus.lottery.infrastructure.dao.IUserTakeActivityDao
@Mapper
public interface IUserTakeActivityDao {

    /**
     * 插入用户领取活动信息
     *
     * @param userTakeActivity 入参
     */
    @DBRouter(key = "uId")
    void insert(UserTakeActivity userTakeActivity);

}

@DBRouter(key = “uId”) key 是入参对象中的属性,用于提取作为分库分表路由字段使用

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

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

相关文章

AI算法创新赛-人车目标检测竞赛总结03

团队简介AI0000032 团队成员均为从事计算机视觉领域的企业员工&#xff0c;热爱技术&#xff0c;勇于挑战&#xff0c;致力于更通用目标检测算法的研究与落地。团队由三人组成&#xff0c;队长何正海 主要负责整体方案设计与模型的量化工作&#xff0c;余洋主要负责模型训练与调…

YOLOV5输出的txt里面有什么猫腻(用于图像分类竞赛中提升图像信息密度)

背景概括&#xff1a; kaggle最近举办了一场医学乳腺癌检测的比赛&#xff08;图像分类&#xff09; 比赛官网地址 给的数据是dcm的专业的医学格式&#xff0c;自己通过DICOM库转为png后&#xff0c;发现该图像胸部不同的患者乳腺大小不一&#xff0c;简言之乳腺的CT有效图在…

MySQL性能调优与设计——MySQL中的索引

MySQL中的索引 InnoDB存储引擎支持以下几种常见索引&#xff1a;B树索引、全文索引、哈希索引&#xff0c;其中比较关键的是B树索引。 B树索引 InnoDB中的索引自然也是按照B树来组织的&#xff0c;B树的叶子节点用来存放数据。 聚集索引/聚簇索引 InnoDB中使用了聚集索引&…

LeetCode 141. 环形链表

原题链接 难度&#xff1a;easy\color{Green}{easy}easy 题目描述 给你一个链表的头节点 headheadhead &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 nextnextnext 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的…

python编写webapi读取mdb数据使用json格式响应客户端请求

想做个自动应答机器人&#xff0c;通过webapi提供服务&#xff0c;原理&#xff1a;判断关键字&#xff0c;到数据库查询相关内容&#xff0c;以json格式反馈给客户端。 1、创建autoreply数据库&#xff0c;创建reply表&#xff0c;表中包含kename&#xff08;短文本&#xff…

积水在线监测仪——积水点、易涝点水位监测设备

一、设备概述 积水在线监测仪是一款用于城市积水点、易涝点等场景的水位监测设备&#xff0c;设备采用电池供电&#xff0c;无需另外供电&#xff0c;安装方便&#xff0c;使用简单。可以时监测水点、易涝点水位情况&#xff0c;当水位数据超过阈值后触发告警上传&#xff0c;…

MybatisPlus实现分页效果并解决错误:cant found IPage for args!

前言 早就知道MybatisPlus对分页进行了处理&#xff0c;但是一直没有实战用过&#xff0c;用的是自己封装的一个分页组件&#xff0c;虽不说麻烦吧&#xff0c;但是也不是特别简单。 写起来还是比较复杂&#xff0c;但是最近这个组件有了点小小的bug&#xff0c;我决定是时候…

2023年进入互联网行业好找工作吗?

俗话说&#xff1a;选择大于努力。年后求职小高峰&#xff0c;大家在找工作的时候选择肯定也多了。 说真&#xff0c;不是人人都有铁饭&#xff0c;普通家庭的孩子想要在2023年进入互联网行业去找工作可能吗&#xff1f; 01 有一点大家要清楚&#xff0c;2022年是进入过一个寒…

【Linux】变量定义规则、shell 格式、空格注意事项汇总

文章目录1. 空格问题号用于赋值用于比较2. 变量2.1 变量命名的格式要求2.2、shell变量中的注意事项2.3、变量的使用方法2.4、变量的类型&#xff08;1&#xff09;自定义变量&#xff08;2&#xff09;环境变量&#xff08;3&#xff09;位置变量&#xff08;4&#xff09;预定…

【高并发-用户中心】读多写少的系统如何优化

本博客纯属个人总结&#xff0c;非原创。喜欢技术交流的&#xff0c;可关注博主&#xff0c;武汉有后端开发群&#xff0c;可支持内推&#xff0c;了解武汉行情等。 如何对读多写少的系统进行高并发优化&#xff1f; 比如&#xff1a;用户中心是一个读多写少的系统&#xff0…

Linux定时备份MySql数据库

一、创建文件 cd / mkdir mysqlbackup vi mysqlbackup.sh然后将下面的代码更改后复制上去即可。 #!/bin/bash mysqldump -uroot -ppassword database > /mysqlbackup/database__$(date %Y%m%d_%H%M%S).sqlpassword指的是MySql的密码&#xff0c;database指的是所要备份的…

【C++算法】dfs深度优先搜索(上) ——【全面深度剖析+经典例题展示】

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f4d5; ps:七八天没更新了欸&#xff0c;这几天刚搞完元宇宙&#xff0c;上午一直练&#x1f697;&#xff0c;下午背四级单词和刷题来着&#xff0c;还在忙一些学弟…

leaflet: 禁止拖拽、禁止zoom(双击、滚轮、键盘)、禁止tap(076)

第076个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中设定各种禁止状态,这里设置了禁止拖拽、禁止zoom(双击、滚轮、键盘)、禁止tap。 直接复制下面的 vue+leaflet源代码,操作2分钟即可运行实现效果 文章目录 示例效果配置方式示例源代码(共73行)相关A…

linux高级命令之多进程的使用

多进程的使用学习目标能够使用多进程完成多任务1 导入进程包#导入进程包import multiprocessing2. Process进程类的说明Process([group [, target [, name [, args [, kwargs]]]]])group&#xff1a;指定进程组&#xff0c;目前只能使用Nonetarget&#xff1a;执行的目标任务名…

电商导购CPS,京东联盟如何跟单实现用户和订单绑定

前言 大家好&#xff0c;我是小悟 做过自媒体的小伙伴都知道&#xff0c;不管是发图文还是发短视频&#xff0c;直播也好&#xff0c;可以带货。在你的内容里面挂上商品&#xff0c;你自己都不需要囤货&#xff0c;如果用户通过这个商品下单成交了&#xff0c;自媒体平台就会…

【刷题笔记】--搜索二维矩阵 II

题目&#xff1a; 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性&#xff1a; 每行的元素从左到右升序排列。 每列的元素从上到下升序排列。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16…

设计师都在看的全球设计网站,你居然还不知道!

设计师需要拥有无限的创意和熟练的技巧&#xff0c;并且对行业的前景和客户的心理有一定的了解。要能达到“陌生化”之前&#xff0c;肯定是有知识储备&#xff0c;专业能力的前提要求&#xff0c;以及创新能力。 今天为大家整理了多个优秀全球设计网站&#xff0c;这些博客内…

大家都在聊的自动化办公到底是什么?

自动化办公无非是excel、ppt、word、邮件、文件处理、数据分析处理、爬虫这些&#xff0c;下面就详细介绍一下&#xff01;文章最后分享了很不错的python学习教程&#xff0c;适合零基础初学的小伙伴&#xff0c;希望可以对你有所帮助&#xff01;&#xff01; excel自动化 我…

linux基本功系列之grep命令

文章目录前言一. grep命令介绍二. 语法格式及常用选项三. 参考案例3.1 搜索文件中以root开头的文件3.2 搜索文件中出现的root3.3 搜索除了匹配行之外的行3.4 匹配的部分使用颜色显示3.5 只输出文件中匹配到的地方3.6 输出包含匹配字符串的行&#xff0c;并显示所在的行数3.7 统…

Unity CircleLayoutGroup 如何实现一个圆形自动布局组件

文章目录简介实现原理Editor 编辑器简介 Unity中提供了三种类型的自动布局组件&#xff0c;分别是Grid Layou Group、Horizontal Layout Group、Vertical Layout Group&#xff0c;本文自定义了一个圆形的自动布局组件Circle Layout Group&#xff0c;如图所示&#xff1a; Ra…