设计一个数据库路由组件实现分库分表

需求设计

随着业务体量的增加,原有的技术设计无法满足现有的需求和规模,分库分表是一种重要的数据库优化技术,通过将数据分散到多个数据库和表中,可以显著提高系统的性能、扩展性、可用性和数据安全性。在设计大型系统时,分库分表是一个值得考虑的重要策略。

方案设计

使用Springboot进行开发,基于aop,Spring动态数据源切换,扰动函数实现整体功能。

实现

读取配置信息

编写DataSourceAutoConfig类,它实现了 EnvironmentAware 接口,
这意味着 Spring 会在配置类被初始化时通过setEnvironment方法注入当前的 Environment,允许类访问配置文件中的属性。

@Configuration
public class DataSourceAutoConfig implements EnvironmentAware {
//创建一个map集合存放数据源
private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();


private Map<String, Map<String, Object>> dataSourceMap = new HashMap<>();

    private int dbCount;  //分库数
    private int tbCount;  //分表数

//实例化一个DBRouterConfig并返回读取到的dbCount,tbCount值
    @Bean
    public DBRouterConfig dbRouterConfig() {
        return new DBRouterConfig(dbCount, tbCount);
    }
 
//通过 setEnvironment 方法读取配置文件中的属性。
    @Override
    public void setEnvironment(Environment environment) {
        String prefix = "router.jdbc.datasource.";
     //读取 dbCount 和 tbCount 作为数据库路由的配置信息
        //2和4
        dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
        tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));

        //db01 ,db02
        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);
        }
    }
}

//被读取的application.yml
# 路由配置
router:
  jdbc:
    datasource:
      dbCount: 2
      tbCount: 4
      list: db01,db02
      db01:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/cool_01?useUnicode=true
        username: root
        password: 123456
      db02:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/cool_02?useUnicode=true
        username: root
        password: 123456

通过以上方法的编写,配置文件中的数据库被读取,并存入dataSourceMap中。

创建数据源

通过上一步获取到的数据库配置,编写new DriverManagerDataSource()用来创建数据源,再放入DynamicDataSource类中,这个类继承了AbstractRoutingDataSource,完成数据源s

@Bean
public DataSource dataSource() {
    // 根据读取的多个数据库信息创建数据源
    Map<Object, Object> targetDataSources = new HashMap<>();
    for (String dbInfo : dataSourceMap.keySet()) {
        Map<String, Object> objMap = dataSourceMap.get(dbInfo);
        //取到了数据源的url,userName,password等信息。
        targetDataSources.put(dbInfo, new DriverManagerDataSource(objMap.get("url").toString(), objMap.get("username").toString(), objMap.get("password").toString()));
    }
    // 设置数据源
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    //DynamicDataSource继承了AbstractRoutingDataSource,这样完成了多个数据源的映射。
    dynamicDataSource.setTargetDataSources(targetDataSources);
    return dynamicDataSource;
}


    //DynamicDataSource类,继承了AbstractRoutingDataSource 
public class DynamicDataSource extends AbstractRoutingDataSource {


    }

 

编写注解

@Documented
@Retention(RetentionPolicy.RUNTIME)// 表示该注解的生命周期将保留到运行时,也就是说,在运行时可以通过反射机制获取并使用该注解。
@Target({ElementType.TYPE, ElementType.METHOD}) //表示该注解可以应用于类和方法上。
public @interface DBRouter {

    String key() default "";

}

基于HashMap实现

通过aop切面拦截注解的方式,实现将使用注解的方法拦截,获取key值,对其进行扰动函数的计算,算出库表各自的索引,存入ThreadLocal中。ThreadLocal 是一种线程局部变量的实现,它为每个线程提供了独立的副本。在本例中,DBContextHolder 使用 ThreadLocal 来为每个线程存储表索引。

@Around("aopPoint() && @annotation(dbRouter)")
//定义了一个环绕通知方法 doRouter,它将在匹配的方法执行前后执行。
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
    String dbKey = dbRouter.key();
    if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");
    // 计算路由
    //deKeyAttr代表的就是UserId
    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();
    }
}

private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
    Signature sig = jp.getSignature();
    MethodSignature methodSignature = (MethodSignature) sig;
    return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}

public String getAttrValue(String attr, Object[] args) {
    String filedValue = null;
    for (Object arg : args) {
        try {
            //BeanUtils.getProperty(arg, attr) 方法尝试从 arg 对象中获取名为 attr 的属性值。
            //举例,此时arg 就是 user 对象,attr 就是 "userId"。
            if (StringUtils.isNotBlank(filedValue)) break;
            filedValue = BeanUtils.getProperty(arg, attr);
        } catch (Exception e) {
            logger.error("获取路由属性值失败 attr:{}", attr, e);
        }
    }
    return filedValue;
}

 

分库分表索引计算

为什么要使用扰动函数计算?因为hashCode太大了,hashCode的取值范围是[-2147483648, 2147483647],数组初始化做不到这么大,内存也放不下。在此组件中我通过一次扰动计算,(dbKeyAttr.hashCode() >>> 16)。把哈希值右移16位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了随机性。再通过 (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16))进行与运算得到了idx,而此处idx的最大值为 size-1。如果 size 为 8,那么 idx 的最大值为 7。如果 size 为 16,那么 idx 的最大值为 15。

那么分库分表的索引怎么计算得来的呢?以八个表为例:把计算出的索引位置 idx 分摊到8个表中,这个时候不需要在使用其他模数,只需要按照HashMap的数据结构分散到库表中即可。 2. 比如:idx = 3,那么它就是在1库到3表、idx = 7 那么它就是在2库的3表,因为1库4个+2库3个正好是7 。

int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 数据库,表索引的计算,基于idx
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);

数据源选择

当进行数据库操作时,上一步中计算好的库表索引就派上用场了,

当一个带有 DBRouter 注解的方法被调用时,aop会拦截方法,根据注解的配置计算出当前请求需要使用的数据库索引,并将其设置到 DBContextHolder 中。当需要执行数据库操作时,DynamicDataSource 的 determineCurrentLookupKey 方法会被调用。根据 DBContextHolder 中的数据库索引, determineCurrentLookupKey 方法返回一个键,如 "db01"。
AbstractRoutingDataSource 会根据返回的键从 targetDataSources 映射中选择对应的数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() { 
         //获取数据库索引
        return "db" + DBContextHolder.getDBKey();
    }
}

 同时mybatis也通过DBContextHolder获取到tbIdx数据表的具体索引并进行具体的操作

<insert id="insertUser" parameterType="cn.bugstack.middleware.test.infrastructure.po.User">
    insert into user_${tbIdx} (id, userId, userNickName, userHead, userPassword,createTime, updateTime)
    values (#{id},#{userId},#{userNickName},#{userHead},#{userPassword},now(),now())
</insert>

基于以上的方法实现,就可以成功通过注解实现动态的分库分表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值