SpringBoot 中间件设计和开发【分库分表组件简易版】

需求背景

数据库路由的需求背景主要来自于业务体量的增加,让原有的技术设计和实现不能承载现有增加的业务规模和体量,因此需要设计分库分表。
最终无论你的应用程序是在最初设计时就开量要分库分表,还是因为业务体量的增加而分库分表,都需要考虑运用什么组件、使用什么方式,来分库分表。可能你会想到 ShardingSphere、MyCat 类似这样的组件。
本文只是个人学习简单设计一个路由组件。

方案设计

  1. 数据库连接池的配置,分库分表需要按需配置数据库连接源,在这些连接池的集合中进行动态切换操作。
  2. AbstractRoutingDataSource,是用于动态切换数据源的 Spring 服务类,它提供了获取数据源的抽象方法 determineCurrentLookupKey
  3. 关于路由的计算。在路由计算中需要获取用于分库分表的字段,通过哈希值的计算以及扰动最终达到尽可能的散列,让数据均匀分散到各个库表中。

技术实现

自定义注解实现标注需要分库分表的方法

注解增强实现分库分表标识

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouter {
   // 通过配置切面拦截当前注解然后对dao进行增强动态选择datasource
   String key() default "";
}

DBRouterStrategy注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DBRouterStrategy {
   boolean splitTable() default false;
}

使用方式:

在dao中将需要进行分库分表的方法加入注解

@Mapper
@DBRouterStrategy(splitTable = true)
public interface IRaffleActivityOrderDao {
   
   @DBRouter(key = "userId")
   void insert(RaffleActivityOrder raffleActivityOrder);

   @DBRouter
   List<RaffleActivityOrder> queryRaffleActivityOrderByUserId(String userId);
   
}

获取配置的多数据源

在中间件的主配置类上实现EnvironmentAware注解读取配置在application.yml中的多数据源
读取之后存入到本地对象中然后创建动态数据源的bean
DynamicDataSource 实现 AbstractRoutingDataSource来进行多数据源配置

public class DynamicDataSource extends AbstractRoutingDataSource {
    // 返回数据源标识
    @Override
    protected Object determineCurrentLookupKey() {
        return "db" + DBContextHolder.getDBKey();
    }
}

其中determineCurrentLookupKey决定了动态选择哪一个数据源,关联到的是一个ThreadLocal封装类,关于这个类中值的设定就是通过第一步中标注注解的切面类实现的下面介绍注解的切面类。

注解对应的切面

DBRouterJoinPoint实现@Aspect
通过拦截当前注解的方法对dao中的方法进行增强,具体的增强也就是设置上一步中用于分库分表的threadlocal中的值。

@Pointcut("@annotation(com.rzp.middleware.db.router.annotation.DBRouter)")
    public void aopPoint() {
    }

    @Around("aopPoint() && @annotation(dbRouter)")
    public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
        String dbKey = dbRouter.key();
        if (StringUtils.isBlank(dbKey) && StringUtils.isBlank(dbRouterConfig.getRouterKey())) {
            throw new RuntimeException("annotation DBRouter key is null!");
        }
        dbKey = StringUtils.isNotBlank(dbKey) ? dbKey : dbRouterConfig.getRouterKey();
        // 路由属性
        String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
        // 路由策略
        dbRouterStrategy.doRouter(dbKeyAttr);
        // 返回结果
        try {
            return jp.proceed();
        } finally {
            dbRouterStrategy.clear();
        }
    }

具体的分库分表值的计算这里借鉴HashMap中的实现方式也就是 h ^ (h >>> 16) & (size - 1)
这里使用到了Strategy模式方便拓展。

@Override
public void doRouter(String dbKeyAttr) {
    // size = 4; 现在备选一个库,一个库里要分表的存在4张表
    // dbCount: 1, tbCount: 4.
    int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();

    // 扰动函数 使用hashMap的扰动函数.
    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("%03d", tbIdx));
    logger.debug("数据库路由 dbIdx:{} tbIdx:{}",  dbIdx, tbIdx);
}

解释:
X % 2^n = X & (2^n - 1) 为什么呢?
首先举例说明当n = 3时,

  • 对于右边来说 2^3 - 1 就相当于是 8 -1 也就是7, 8的二进制表示为 1000,7为0111,也就是将低位全部变为1然后 & X,就相当于取X的低三位。
  • 对于左边来说,我们先思考X / 2^3 其实就相当于 X >>> 3位,那么X % 2^n 其实也就相当于获取X的低三位。
  • 因为&是二进制直接计算效率比十进制取模高!!。

为什么要进行 (h ^ h >>> 16)呢为什么不是直接 h & (size - 1)?
考虑当前size很小时,因为size很小所以size - 1的二进制表示高位基本全为0,那么在和h进行&操作时很难考虑到h二进制中的高位,因此增加一层扰动让h ^ h >>> 16充分融合低位和高位的值,再进行 & 操作。

因此对于这个自研的组件来说也必须保证数据库数量 * 表的size = 2 ^ n;

Mybatis插件拦截进行具体执行

通过使用Mybatis提供的插件拦截sql,因为已经设置了动态数据源,所以还需要动态的去拦截sql,用户在使用时仍然只需要配置一个xml文件,mybatis插件拦截sql,然后重新组装成我们计算好的数据库名称。
通过实现Interceptor接口来进行插件开发。

@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 在intercept方法中,首先获取到被拦截的StatementHandler对象和相关的元数据信息。
    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();
    // 拿到当前dao类对象的名字
    String className = id.substring(0, id.lastIndexOf("."));
    Class<?> clazz = Class.forName(className);
    // 查看当前dao对象是否加了自定义的注解
    DBRouterStrategy dbRouterStrategy = clazz.getAnnotation(DBRouterStrategy.class);
    // 如果使用了DBRouterStrategy注解并且splitTable属性为true,则获取当前SQL语句。
    if (null == dbRouterStrategy || !dbRouterStrategy.splitTable()){
        return invocation.proceed();
    }

    BoundSql boundSql = statementHandler.getBoundSql();
    String sql = boundSql.getSql();

    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());
    Field field = boundSql.getClass().getDeclaredField("sql");
    field.setAccessible(true);
    field.set(boundSql, replaceSql);
    field.setAccessible(false);
    // 最后调用invocation.proceed()方法继续执行原始的数据库操作。
    return invocation.proceed();
}

提供事务管理

@Bean
public TransactionTemplate transactionTemplate(DataSource dataSource){
    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
    dataSourceTransactionManager.setDataSource(dataSource);
    TransactionTemplate transactionTemplate = new TransactionTemplate();
    transactionTemplate.setTransactionManager(dataSourceTransactionManager);
    transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");
    return transactionTemplate;
}
  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于Spring Boot分库分表的项目实战,你可以按照以下步骤进行: 1. 配置数据源:在Spring Boot项目的配置文件中,添加多个数据源配置,用于连接不同的数据库。可以使用Spring Boot提供的多数据源配置,或者自己实现多数据源的Bean。 2. 配置分库分表策略:选择合适的分库分表策略,常见的有垂直分库、水平分库、垂直分表、水平分表等。根据具体业务需求,配置相应的分库分表规则。 3. 使用分库分表中间件:引入适合的分库分表中间件,如ShardingSphere、MyCAT等。根据中间件的文档和配置方式,进行相应的配置,使得中间件能够根据配置的规则将数据进行分库分表。 4. 数据访问层设计:在数据访问层(DAO)中,需要根据具体的业务需求,按照分库分表规则进行查询和操作。可以使用中间件提供的API或者自行编写相关代码。 5. 单元测试和压力测试:在开发过程中,务必编写相应的单元测试用例,验证分库分表的功能是否正常。同时,进行压力测试,模拟多种并发情况下的数据库访问,查看系统的性能是否满足需求。 6. 监控和调优:在项目上线后,需要进行系统的监控和性能调优。监控数据库的负载情况,根据实际情况进行调整分库分表的配置和规则,以保证系统的稳定性和性能。 以上是Spring Boot分库分表的一个简单实战流程,具体的实现方式会根据业务需求和技术栈的不同而有所差异。希望对你有所帮助!如果有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值