• 分库分表路由组件构建方案V1


    路由组件构建方案V1

    实现效果:通过注解实现数据分散到不同库不同表的操作。
    实现主要以下几部分:

    1. 数据源的配置和加载
    2. 数据源的动态切换
    3. 切点设置以及数据拦截
    4. 数据的插入

    涉及的知识点:

    1. 分库分表相关概念
    2. 散列算法
    3. 数据源的切换
    4. AOP切面
    5. Mybatis拦截器

    数据源的配置和加载

    获取多个数据源我们肯定需要在yaml或者properties中进行配置。所以首先需要获取到配置信息;
    定义配置文件中的库和表:

    server:
      port: 8080
    # 多数据源路由配置
    router:
      jdbc:
        datasource:
          dbCount: 2
          tbCount: 4
          default: db00
          routerKey: uId
          list: db01,db02
          db00:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
            username: xxxx
            password: 111111
          db01:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://xxxxx:3306/xxxxx?useUnicode=true
            username: xxxxx
            password: 111111
          db02:
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://xxxxx:3306/xxxx?useUnicode=true
            username: xxxxx
            password: 111111
    mybatis:
      mapper-locations: classpath:/com/xbhog/mapper/*.xml
      config-location:  classpath:/config/mybatis-config.xml
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    为了实现并且使用自定义的数据源配置信息,启动开始的时候让SpringBoot定位位置。
    首先类加载顺序:指定自动配置;

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.db.router.config.DataSourceAutoConfig
    
    • 1

    针对读取这种自定义较大的信息配置,就需要使用到 org.springframework.context.EnvironmentAware 接口,来获取配置文件并提取需要的配置信息。

    public class DataSourceAutoConfig implements EnvironmentAware {
    
        @Override
        public void setEnvironment(Environment environment){
            ......
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    属性配置中的前缀需要跟路由组件中的属性配置:
    这里设置成什么,在配置文件中就要设置成对应名字

    String prefix = "router.jdbc.datasource.";
    
    • 1

    根据其前缀获取对应的库数量dbCount、表数量tbCount以及数据源信息dataSource;

    //库的数量
    dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
    //表的数量
    tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
    //分库分表数据源
    String dataSources = environment.getProperty(prefix + "list");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    针对多数据源的存在,使用Map进行存储:Map> daraSources;

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

    通过dataSource方法实现数据源的实例化:把基于从配置信息中读取到的数据源信息,进行实例化创建。
    将获得的信息放到DynamicDataSource类(父类:DataSource)中进行实例化(setTargetDataSources,setDefaultTargetDataSource);
    将我们自定义的数据源加入到Spring容器管理中。

    //创建数据源
    Map<Object, Object> targetDataSource = new HashMap<>();
    //遍历数据源的key和value
    for(String dbInfo : dataSourceMap.keySet()){
        Map<String, Object> objectMap = dataSourceMap.get(dbInfo);
        targetDataSource.put(dbInfo,new DriverManagerDataSource(objectMap.get("url").toString(),
                objectMap.get("username").toString(),objectMap.get("password").toString()));
    }
    //这是数据源
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    dynamicDataSource.setTargetDataSources(targetDataSource);
    //defaultDataSourceConfig的输入点
    dynamicDataSource.setDefaultTargetDataSource(new DriverManagerDataSource(defaultDataSourceConfig.get("url").toString(),
            defaultDataSourceConfig.get("username").toString(),defaultDataSourceConfig.get("password").toString()));
    return dynamicDataSource;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    到这里前置的配置都在spring中完成,后续是对数据的插入,也就是mybatis的操作:包含库表的随机计算和数据拦截器的实现。

    动态切换数据源

    路由切换的实现通过AbstractRoutingDataSource抽象类,该类充当了DataSource的路由中介, 在运行的时候, 根据某种key值来动态切换到真正的DataSource上。继承了AbstractDataSourceAbstractDataSource实现了DataSource;
    AbstractRoutingDataSource根据方法determineTargetDataSource:

    检索当前目标数据源。确定当前查找键,在targetDataSources映射中执行查找,必要时退回到指定的默认目标数据源。

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        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 + "]");
        }
        return dataSource;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    里面使用determineCurrentLookupKey方法来确定当前查找的键(数据源key);

    抽象方法determineCurrentLookupKey()返回DataSource的key值,然后根据这个key从resolvedDataSources这个map里取出对应的DataSource,如果找不到,则用默认的resolvedDefaultDataSource

    	/**
    	 *确定当前查找键。这通常用于检查线程绑定的事务上下文。 
    	 *允许任意键。返回的键需要匹配由resolveSpecifiedLookupKey方法解析的存储查找键类型
    	 */
    	@Nullable
    	protected abstract Object determineCurrentLookupKey();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    所以我们只需要重写determineCurrentLookupKey,指定我们切换数据源的名字即可;

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return "db"+ DBContextHolder.getDBKey();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这部分对应上了前面创建数据源的操作,实现的该DynamicDataSource,并传入了默认数据源(setDefaultTargetDataSource)和目标数据源(setTargetDataSources);

    自定义切点

    前期数据源的配置和信息已经放到Spring容器中,可随时使用;根据注解通过拦截器拦截方法中的数据。进行分库分表的操作,通过扰动函数进行计算,将结果保存到ThreadLocal中,方便后续读取。

    注解实现:

    分库注解:首先设置三要素。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE,ElementType.METHOD})
    public @interface DBRouter {
    
        /** 分库分表字段 */
        String key() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过自定义切点@Around(**"aopPoint()&&@annotation(dbRouter)"**),实现使用注解的时候就拦截对应的值:
    在环绕处理的时候,判断方法上注解是否对应有值,有的话通过注解传入的value和方法传入的参数进行路由计算:
    计算规则:

    1. 获取方法传入的参数
    2. 计算库表总数量:dbCount*tbCount
    3. 计算idx:**int **idx = (size -1) & (Key.hashCode() ^ (Key.hashCode() >>> 16))
      1. 简单说明:与运算标识符后面,通过混合高位和低位,增大随机性
    4. **int **dbIdx = idx / dbCount() + 1
    5. **int **tbIdx = idx - tbCount() * (dbIdx - 1)

    通过上述操作,将计算的记过保存到ThreadLocal中。
    获取方法传入的参数:

    private String getAttrValue(String dbKey, Object[] args) {
        if(1 == args.length){
            return args[0].toString();
        }
        String filedValue = null;
        for(Object arg : args){
            try{
                if(StringUtils.isNotBlank(filedValue)){
                    break;
                }
                filedValue = BeanUtils.getProperty(arg,dbKey);
            }catch (Exception e){
                log.info("获取路由属性失败 attr:{}", dbKey,e);
            }
        }
        return filedValue;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    自定义拦截器

    我们定义了Interceptor将拦截StatementHandler(SQL语法构建处理拦截)中参数类型为Connection的prepare方法,具体需要深入mybatis源码;
    主要功能:在执行SQL语句前拦截,针对相关功能实现SQL的修改
    在上述文章中主要是针对分库分表前做准备,下面才是决定数据入哪个库哪张表
    通过StatementHandler(MyBatis直接在数据库执行SQL脚本的对象)获取mappedStatement(MappedStatement维护了一条节点的封装),根据maperdStatement获取自定义注解dbRouterStrategy,判断是否进行分表操作;

    Class<?> clazz = Class.forName(className);
    DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
    if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
        return invocation.proceed();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    dbRouterStrategy注解默认是false不分表,直接进行数据的插入【更新】;

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE,ElementType.METHOD})
    public @interface DBRouterStrategy {
        boolean splitTable() default false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果分表注解存在或者分表参数是true,则进行以下四步:

    1. 获取SQL

      BoundSql:表示动态生成的SQL语句以及相应的参数信息。

    //获取SQL
    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();
    
    • 1
    • 2
    • 3
    1. 匹配SQL

    通过正则匹配分割【insert/select/update】和表名,方便后续表名的拼接。

    //替换SQL表名USER为USER_3;
    Matcher matcher = pattern.matcher(sql);
    String tableName = null;
    if(matcher.find()){
        tableName = matcher.group().trim();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. 拼接SQL

    则通过反射修改SQL语句,并且替换表名;其中filed.set()将指定对象实参上由此field对象表示的字段设置为指定的新值。如果基础字段具有基元类型,则自动解开新值

    assert null != tableName;
    String replaceSQL = matcher.replaceAll(tableName + "_" + DBContextHolder.getTBKey());
    //通过反射修改SQL语句
    Field filed = boundSql.getClass().getDeclaredField("sql");
    filed.setAccessible(true);
    filed.set(boundSql,replaceSQL);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参考文章

    https://www.cnblogs.com/aheizi/p/7071181.html

    https://blog.csdn.net/wb1046329430/article/details/111501755

    https://blog.csdn.net/supercmd/article/details/100042302

    https://juejin.cn/post/6966241551810822151

  • 相关阅读:
    Spark Streaming 基本操作
    springboot+影院售票小程序 毕业设计-附源码111154
    白炽灯和led哪个护眼?分享真正适合孩子的护眼台灯
    基于形态学重建和过滤改进FCM算法实现图像分割
    Spring cloud 微服务跨域问题
    vscode access denied to unins000.exe
    以训辅教,以战促学 | 新版攻防世界平台正式上线运营!
    JAVA计算机毕业设计手办销售系统源码+系统+mysql数据库+lw文档
    什么是R-tree?
    《Linux》day3--shell语法(上)
  • 原文地址:https://blog.csdn.net/weixin_43908900/article/details/127423915