自定义分库分表组件

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 自定义分库分表组件

上一节讲到了分库分表的的实现原理,本质就是通过先改写sql语句,然后拿到数据库执行完毕再返回

分库分表后,如果根据sql语句找到对应的库和表呢

  1. 首先在配置文件配置多数据源(多库),获取配置信息注入到系统中
  2. 通过AOP切面在执行的sql的上面加上自定义注解,还有标注分库、分表的字段
  3. AOP进行拦截根据路由字段进行计算到具体的数据源,这里涉及到分库分表的算法选择
  4. 在sql执行的时候通过Mybatis拦截器获取sql语句然后替换表名称

大致步骤明白了,接下来看如何实现吧

分库分表设计概要

水平分库拆分为两个库mall_1、mall_2,每个库2个表orders0、orders1

CREATE DATABASE `mall_1`;
-- mall_1.orders0 definition
CREATE TABLE `orders0` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_number` bigint NOT NULL,
  `create_time` date NOT NULL,
  `creater` varchar(20) NOT NULL,
  `money` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1553314824824299523 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- mall_1.orders1 definition
CREATE TABLE `orders1` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_number` bigint NOT NULL,
  `create_time` date NOT NULL,
  `creater` varchar(20) NOT NULL,
  `money` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1553314460645474306 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE DATABASE `mall_2`;
CREATE TABLE `orders0` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_number` bigint NOT NULL,
  `create_time` date NOT NULL,
  `creater` varchar(20) NOT NULL,
  `money` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1553314824824299523 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- mall_1.orders1 definition
CREATE TABLE `orders1` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_number` bigint NOT NULL,
  `create_time` date NOT NULL,
  `creater` varchar(20) NOT NULL,
  `money` decimal(10,0) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1553314460645474306 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

多数据源配置

一个库对应一个数据源,所以需要配置多数据源,然后在程序启动的时候将数据库信息注入进来

server.port=8099
spring.application.name=sub-db-sub-table
spring.profiles.active=dev
#多数据源
#db1
spring.datasource.db0.jdbcUrl=jdbc:mysql://121.43.33.150:3306/mall_1?serverTimezone=GMT%2B8
spring.datasource.db0.username=root
spring.datasource.db0.password=xxx
spring.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
#db2
spring.datasource.db1.jdbcUrl=jdbc:mysql://121.43.33.150:3306/mall_2?serverTimezone=GMT%2B8
spring.datasource.db1.username=root
spring.datasource.db1.password=xxx
spring.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.db.count=2
spring.datasource.table.count=4
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath*:mybatis/mapper/*.xml
logging.level.com.ylc.dao=DEBUG

拿到配置文件信息可以通过实现Spring的EnvironmentAware接口,然后通过@Configuration注解把配置信息作为Bean对象加载到Spring容器中

数据源配置关键代码

通过@ConfigurationProperties注解,根据前缀获取到数据库配置

@Bean("db0")
    @ConfigurationProperties(prefix = "spring.datasource.db0")
    public DataSource db0() {
        return DataSourceBuilder.create().build();
    }
    @Bean("db1")
    @ConfigurationProperties(prefix = "spring.datasource.db1")
    public DataSource db1() {
        return DataSourceBuilder.create().build();
    }
    /**
     * 动态数据源: 通过AOP在不同数据源之间动态切换
     * @return
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 默认数据源
        dynamicDataSource.setDefaultTargetDataSource(db0());
        // 配置多数据源
        Map<Object, Object> dsMap = new HashMap<>(8);
        dsMap.put("db1", db0());
        dsMap.put("db2", db1());
        dynamicDataSource.setTargetDataSources(dsMap);
        return dynamicDataSource;
    }
    @Override
    public void setEnvironment(Environment environment) {
        dbCount = Integer.valueOf(environment.getProperty("spring.datasource.db.count"));
        tableCount = Integer.valueOf(environment.getProperty("spring.datasource.table.count"));
    }

以上演示了获取配置文件的两种方法:实现EnvironmentAware接口、使用@ConfigurationProperties注解

数据源切换

在分库的时候需要切分不同的数据库,可以通过实现AbstractRoutingDataSource类来实现,这个类是DataSource接口的实现类,通过重写determineCurrentLookupKey方法来实现这个效果,实际就是根据之前注入的DynamicDataSource对象来进行路由定位数据源的。

/**
 * 动态数据源获取  能根据key动态切换数据库
 * @author yanglingcong
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return "db"+MultiDataSourceHolder.getDataSourceKey();
    }
}

自定义注解+AOP切面实现分库操作

自定义注解

用于存储分表字段的注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DBRouter {
    /** 分库分表字段 */
    String key() default "";
}

AOP切面实现

AOP用于在使用自定义注解的方法上,通过拦截方法获取分片字段还有传入的值,进行数据库路由计算

package com.ylc.ascept;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ylc.annotation.DBRouter;
import com.ylc.config.MultiDataSourceHolder;
import com.ylc.strategy.IDBRouterStrategy;
import com.ylc.strategy.impl.DBRouterStrategyHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
 * @author yanglingcong
 */
@Component
@Slf4j
@Aspect
public class AsceptRoute {
    @Autowired
    private IDBRouterStrategy dbRouterStrategy;
    @Pointcut("@annotation(com.ylc.annotation.DBRouter)")
    public void pointCut() {
    }
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
    }
    @Around("pointCut()")
    public Object doRouter(ProceedingJoinPoint jp) throws Throwable {
        //获取方法调用名称
        Method method = getInvokeMethod(jp);
        //获取方法指定的注解
        DBRouter router = method.getAnnotation(DBRouter.class);
        //获取指定的路由key
        String dbKey = router.key();
        if (StringUtils.isBlank(dbKey)) {
            throw new RuntimeException("annotation DBRouter key is null!");
        }
        // 获取路由字段属性值
        String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
        // 路由策略
        dbRouterStrategy.doRouter(dbKeyAttr);
        // 返回结果
        return jp.proceed();
    }
    private Method getInvokeMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature)signature;
        Method targetMethod = methodSignature.getMethod();
        return targetMethod;
    }
    @After("pointCut()")
    public void  methodAfter(JoinPoint joinPoint){
        MultiDataSourceHolder.clearDataSourceKey();
        MultiDataSourceHolder.clearTableIndex();
    }
}

路由策略

@Override
    public void doRouter(String dbKeyAttr) {
        int size = dataSourseConfig.getDbCount() * dataSourseConfig.getTableCount();
        //扰动函数
        int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
        // 库表索引
        int dbIdx = idx / dataSourseConfig.getTableCount() ;
        int tbIdx = idx - dataSourseConfig.getTableCount() * (dbIdx - 1);
        // 设置到 ThreadLocal
        MultiDataSourceHolder.setdataSourceKey(String.valueOf(dbIdx));
        MultiDataSourceHolder.setTableIndex(String.valueOf(tbIdx));
        log.info("数据库路由 dbIdx:{} tbIdx:{}",  dbIdx, tbIdx);
    }

1、通过AOP拦截获取分片字段还有值

2、拿到信息之后进行,这里使用了HashMap扰动函数、哈希索引进行库表索引的计算,使数据更加分散

3、然后把索引数据存入ThreadLocal中,使得在请求线程中可以获取到相关信息

4、最后调用方法完毕后清理ThreadLocal,为了避免内存泄漏

分库分表算法选择

分库分表要使得数据尽量分散,所以一般采用散列类型的算法

Hash算法

常见于HashMap集合实现:数组+链表+红黑树,为了将元素的位置更加散列用到了扰动函数,在存放元素的时候用到了这样一段代码

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

把哈希值右移16位正好是自己的一半,然后通过异或运算增加随机性,减少碰撞

斐波那契数列黄金分割

常见于ThreadLocal数组,它底层是一个类似于链表的结构,也叫拉链存储,里面也用到了Hahs计算不同的是它用的自己实现的算法

private static final int HASH_INCREMENT = 0x61c88647;
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

每创建一个对象,值就会增长 0x61c88647,为什么是这个值呢,这个值代表的是一个黄金分割树0.6180339887,也叫斐波那契数,就是为了让数据更加分散

Mybatis拦截器实现分表操作

Mybatis支持四种对象拦截Executor、StatementHandler、PameterHandler和ResultSetHandler

  1. Executor:拦截执行器的方法
  2. StatementHandler:拦截Sql语法构建的处理
  3. ParameterHandler:拦截参数的处理
  4. ResultHandler:拦截结果集的处理

可以基于SQL拦截器StatementHandler,通过拦截SQL语句然后修改SQL信息来达到分库分表的操作

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class MybatisInterceptor implements Interceptor {
    //匹配增删改查sql
    //  [\s] 空白
    //  {1,} 匹配最少一个
    //  \w 匹配字母、数字、下划线
    private Pattern pattern = Pattern.compile("(from|into|update)[\\s]{1,}(\\w{1,})", Pattern.CASE_INSENSITIVE);
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        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 className = mappedStatement.getId();
        String cn= className.substring(0, className.lastIndexOf("."));
        //方法名
        String methodName=className.substring(cn.length()+1);
        Class<?> clazz = Class.forName(cn);
        final Method[] method = clazz.getMethods();
        for (Method me : method) {
            if (me.getName().equals(methodName)&&me.isAnnotationPresent(DBRouter.class)) {
                DBRouter dbRouterStrategy = me.getAnnotation(DBRouter.class);
                if (ObjectUtils.isEmpty(dbRouterStrategy.key())){
                    return invocation.proceed();
                }
            }
        }
        BoundSql boundSql = statementHandler.getBoundSql();
        //参数
        Object obj = boundSql.getParameterObject();
        //sql语句
        String sql = boundSql.getSql();
        //匹配sql
        Matcher matcher = pattern.matcher(sql);
        String tableName = null;
        if (matcher.find()) {
            tableName = matcher.group().trim();
        }
        //替换表名
        String replaceSql = matcher.replaceAll(tableName  + MultiDataSourceHolder.getDataSourceKey());
        // 通过反射修改SQL语句
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, replaceSql);
        field.setAccessible(false);
        return invocation.proceed();
    }
}

通过实现Interceptor接口,利用正则表达式匹配当前SQL语句,然后替换掉表名修改表信息再设置回 Mybatis 执行 SQL 中,可以达到分表的效果

效果测试

根据id查询几条数据,在方法上加上@DBRouter注解

@Mapper
public interface OrderMapper extends BaseMapper<Orders> {
    @DBRouter(key = "id")
    Orders selectOneByMap(Orders orders);
}

id=10时,路由到了mall_1库,orders1表

id=1时,路由到了mall_0库,orders0表


相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
小程序 数据格式
【经验分享】如何实现自定义数据源的级联选择组件?
【经验分享】如何实现自定义数据源的级联选择组件?
33 0
|
4月前
|
前端开发
实战:思考如何拆分组件
实战:思考如何拆分组件
|
9月前
|
数据库
分库分表后路由策略设计
分库分表后设计到的第一个问题就是,如何选择路由key,应该如何对key进行路由。路由key应该在每个表中都存在而且唯一。路由策略应尽量保证数据能均匀进行分布。 如果是对大数据量进行归档类的业务可以选择时间作为路由key。比如按数据的创建时间作为路由key,每个月或者每个季度创建一个表。按时间作为分库分表后的路由策略可以做到数据归档,历史数据访问流量较小,流量都会打到最新的数据库表中。 也可以设计其与业务相关的路由key。这样可以保证每个数据库的资源都能很好的承担流量。
|
10月前
|
SQL 算法 Java
自定义水平分库分表策略【范围分片】
自定义水平分库分表策略【范围分片】
|
10月前
|
SQL 算法 Java
自定义水平分库分表策略【hint分片】
自定义水平分库分表策略【hint分片】
|
存储 前端开发 Java
可路由计算引擎实现前置数据库
可路由计算引擎实现前置数据库
可路由计算引擎实现前置数据库
|
存储 SQL 缓存
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
209 0
基于springboot+jpa 实现多租户动态切换多数据源 - 数据隔离方案选择分库还是分表
|
SQL 中间件 关系型数据库
MyCat - 分片 - 垂直拆分 - 全局表配置 | 学习笔记
快速学习 MyCat - 分片 - 垂直拆分 - 全局表配置
114 0
MyCat - 分片 - 垂直拆分 - 全局表配置 | 学习笔记
|
存储 关系型数据库 MySQL
MyCat - 分片 - 垂直拆分 - 概述及案例场景 | 学习笔记
快速学习 MyCat - 分片 - 垂直拆分 - 概述及案例场景
83 0
MyCat - 分片 - 垂直拆分 - 概述及案例场景 | 学习笔记
|
存储 关系型数据库 MySQL
MyCat - 分片 - 水平拆分 - 概述及案例场景 | 学习笔记
快速学习 MyCat - 分片 - 水平拆分 - 概述及案例场景
82 0
MyCat - 分片 - 水平拆分 - 概述及案例场景 | 学习笔记