多租户shardingSphere+mybatis实现实现

多租户shardingSphere+mybatis实现实现

多租户分库分表场景分析

多租户是指软件架构支持一个实例服务多个用户(Customer),每一个用户被称之为租户(tenant),软件给予租户可以对系统进行部分定制的能力,例如数据权限、角色权限等。特别适合于sass化软件及相关租户业务定制的场景。

在多租户的设计上,需要考虑租户数据的物理隔离和逻辑隔离。逻辑隔离一般会用一个tenantId来做sql表数据,代码业务逻辑上来区分不同租户,且一般会放贯穿在整个服务调用链路中。物理隔离指的是数据存储的隔离,一般可以采用分库分表的模式进行隔离数据,这里有这几点好处:

1.租户数据存储在不同的数据库实例中,隔离部署,单机房挂了之后,只影响部分租户数据,做到容灾的效果。

2.数据库到达千万级以上数据之后会带给单库的实例来IO、CPU、内存的上的压力,分库分表实现能减免这种压力,并且具有可拓展性。

shardingSphere+mybatis实现

这里采用shardingSphere+mybatis的jar包方式来实现路由键租户tenantId分库分表,关于shardingSphere的使用https://blog.csdn.net/qq_17236715/article/details/126925494?spm=1001.2014.3001.5502 和 https://blog.csdn.net/qq_17236715/article/details/127680981?spm=1001.2014.3001.5502,文章有详细提交及验证相关sql和使用相关算法。

实现实例采用2X2的分库分表模式,两个库的表结构都是一样的,表结构中都带有tenant_id字段。并且采用tenant_user表作为广播表,存在于所有库中,里面存取的是租户的信息。

路由算法采用hint算法,并统一设置请求拦截器设置租户的路由键,一般租户id会通过请求头、cookie等方式带入到请求信息中,算法中会利用租户id做库的路由和表路由。考虑到是简单的2X2分库分表,这里的路由简单就是按照tenant_id %2的方式计算。请求aop代码如下,租户id是在请求头中。

@Aspect
@Order(-10)
@Component
@Slf4j
public class ShardingJdbcHintRouteAspect {

    @Value("${spring.shardingsphere.sharding.binding-tables}")
    private String shardingTables;
    /**
     * 定义一个方法,用于声明切入点表达式,方法中一般不需要添加其他代码 使用@Pointcut声明切入点表达式 后面的通知直接使用方法名来引用当前的切点表达式;如果是其他类使用,加上包名即可
     */
    @Pointcut("execution(public * com.ilearning.*.controller..*Controller.*(..))")
    public void declearJoinPointExpression() {
    }

    /**
     * 前置通知
     *
     * @param joinPoint
     */
    public void beforMethod(JoinPoint joinPoint) {
        // Hint分片策略必须要使用 HintManager工具类
        HintManager hintManager = HintManager.getInstance();
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String headValue = "";
        if (sra != null) {
            headValue  = sra.getRequest().getHeader("tenantId");
        }

        log.info("sharding table value {}", shardingTables);
        if (StringUtils.isNotBlank(shardingTables)) {
            String[] tables = shardingTables.split(",");
            for (String table : tables) {
                hintManager.addDatabaseShardingValue(table, headValue);
                hintManager.addTableShardingValue(table, headValue);
            }
        }
    }

    /**
     * 后置通知(无论方法是否发生异常都会执行,所以访问不到方法的返回值)
     *
     * @param joinPoint
     */
    @After("declearJoinPointExpression()")
    public void afterMethod(JoinPoint joinPoint) {
        log.info("After---------------");
        HintManager.clear();
    }

    /**
     * 返回通知(在方法正常结束执行的代码) 返回通知可以访问到方法的返回值!
     *
     * @param joinPoint
     */
    public void afterReturnMethod(JoinPoint joinPoint, Object result) {

    }

    /**
     * 环绕通知(需要携带类型为ProceedingJoinPoint类型的参数) 环绕通知包含前置、后置、返回、异常通知;ProceedingJoinPoin 类型的参数可以决定是否执行目标方法 且环绕通知必须有返回值,返回值即目标方法的返回值
     *
     * @param joinPoint
     */
    @Around(value = "declearJoinPointExpression()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) {
        Object result = null;
        // 前置通知
        beforMethod(joinPoint);

        // 执行目标方法
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        // 后置通知 - 新建
        afterReturnMethod(joinPoint, result);
        return result;
    }
}

为了更方便和采用mybatis的租户拦截器实现对sql的拦截,加入tenant_id 字段,将实际业务和租户的sql字段进行解耦,增加可维护性。拦截器组装tenant_id 字段如下:

@Configuration
public class TenantAutoConfiguration {
    @Bean
    public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
                                                                 MybatisPlusInterceptor interceptor) {
        TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
        // 添加到 interceptor 中
        // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
        MyBatisUtils.addInterceptor(interceptor, inner, 0);
        return inner;
    }
}

TenantDatabaseInterceptor 获取请求头的tenantID

@AllArgsConstructor
public class TenantDatabaseInterceptor implements TenantLineHandler {

    private final TenantProperties properties;

    @Override
    public Expression getTenantId() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        String headValue = "";
        if (sra != null) {
            headValue  = sra.getRequest().getHeader("tenantId");
        }
        return new StringValue(headValue);
    }

    @Override
    public boolean ignoreTable(String tableName) {
        return CollUtil.contains(properties.getIgnoreTables(), tableName); // 情况二,忽略多租户的表
    }

}

原始sql,其中 r.tenant_id = ‘56’ 是mysql plus插件组装的

 SELECT l.id, l.user_id, l.status, r.id AS item_id, r.order_id, r.status AS item_status, r.user_id AS item_user_id FROM pay_parent AS l LEFT JOIN pay_parent_item r ON l.id = r.order_id AND r.tenant_id = '56' WHERE 1 = 1 AND l.user_id = ? AND r.user_id = ? AND l.tenant_id = '56' LIMIT ?
c.i.p.d.m.p.P.selectPageDetail           : ==> Parameters: 56(Integer), 56(Integer), 10(Long)

经过分库分表后的sql

ShardingSphere-SQL                       : Actual SQL: demo0 ::: SELECT l.id, l.user_id, l.status, r.id AS item_id, r.order_id, r.status AS item_status, r.user_id AS item_user_id FROM pay_parent_0 AS l LEFT JOIN pay_parent_item_0 r ON l.id = r.order_id AND r.tenant_id = '56' WHERE 1 = 1 AND l.user_id = ? AND r.user_id = ? AND l.tenant_id = '56' LIMIT ? ::: [56, 56, 10]
 <==      Total: 10

总结

租户的分库分表场景适合的业务包括租户的数据量较多,存储遇到瓶颈的情况,分库分表后会遇到如下几个问题,

1.复杂条件查询,例如按照时间查询多个租户的数据,做数据分析或者报表统计,这样分库分表性能还是存在问题

2.租户数据分摊均匀,租户数据是否能按照路由算法均匀分摊在不同的库或者表,取决于算法对于路由键是否均匀

3.每个租户的数据分布不是那么均匀,如何导致某个库的压力比较大,或者后续租户需要扩容,重新分布,如何处理。

对于2、3,在设计路由算法需要考虑路由的均匀性,以及重新扩容或者缩容上的可拓展性,尽量以合适的资源承载均匀的数据。对于1的问题,取决于实际业务的情况,实时性要求高的,可以采用冷热数据库,热裤承载近期的热点数据,实时要求低的也可以采用非关系型数据去存储,做好主备、容灾,异步拉取分库分表的数据。存储的架构的设计是随着业务扩大一步一步演化,需要在实践中一步一步探索适合自身业务的解决方案。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值