Sharding Sphere基于复合分片键分表实战

Posted 老邋遢

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Sharding Sphere基于复合分片键分表实战相关的知识,希望对你有一定的参考价值。

Sharding Sphere基于复合分片键分表实战

文章目录

一. 背景需求

笔者所在系统经过了若干年的数据积累, 出现了众多mysql大表, 这些大表的数据量从 6000w ~ 30000w不等.

经分析, 数据库实例平时使用率不高, 但是单表已经达到瓶颈, 对于单表的频繁复杂查询, 会逐步拖垮数据库实例.

最终决定对大表进行拆分(只分表不分库), 以此来减轻单表压力, 提升系统查询时性能.

二. 分片键选取

2.1 分库/分表时机

一般来说, MySQL推荐的单表数据量在500w ~ 800w, 超过800w则建议分表.

或是在系统接口响应时间明显变慢, 并且通过代码优化, 改写sql等形式无法获得明显提升, 且明确了性能瓶颈在数据库时, 建议分表.

分库则是在分表后单库性能到达瓶颈后进行, 如果个人或项目有钱任性的除外.

2.2 分片键的选取原则

分片键应选取原有的每个sql都有的查询条件(或者是通过改写原有sql能带上的查询条件)

如果不能保证所有sql都有的查询条件作为分片键, 那么至少也要保证绝大部分, 否则分库分表没有任何意义.

一般的分片键可以取id或者创建时间, 类似于更新时间一类频繁变动的字段不适合作为分片键, 分片键应当是长时间保持不变的字段.

另外分片键最好是便于计算, 有一定规律的字段, 利于我们用尽可能简单的算法去实现分片, 简单的算法容错率更高, 执行效率更快.

2.3 分完片的后续工作

分片后面临以下几个主要的工作:

  • 历史数据的迁移
  • sql的重写
  • 代码的重构

其中历史数据的迁移可以使用一些ETL工具或者是DATAX实现.

sql重写要求DBA改写之前的sql来尽可能使用到分片键路由.

代码的重构包括数据源变更, 分片算法实现等.

2.4 数据增长较快系统建议

对于数据增长较快的系统, 在使用传统关系型数据库的前提下, 单表很快就会达到性能瓶颈.

对于这类系统, 应当在设计初期, 及时制定sql规范, 合理设计每张表的分片键, 适当做出一些业务和设计上的妥协.

这样能为将来的平滑分库分表打下基础.当然如果没有强事务要求的系统可以考虑非关系型数据库, 或者支持事务以及天然支持分布式的TiDB.

2.5 分片后的常见问题

分片后的问题很多, 无法一一列举.

一般来说, 系统越庞杂, 分片越晚, 问题就会更多.

当然这里面还有一些现实因素: 没钱加数据库实例, 需求的不妥协, coder们水平的良莠不齐等等.

所以分片不是银弹, 具体的情况还是需要结合实际情况来采取最适合的方案.

以下是笔者遇到或者是业内常见的一些问题:

  • 分片后的数据倾斜

    这个几乎是无法避免的, 即使是id取模, 也会因为数据的删除导致每张分表的数据不一样, 或者id是UUID, 取模也会导致数据发生倾斜.

    但是一般来说倾斜只要不是太离谱, 都在我们的接受范围以内.

  • 分片后的id生成策略

    如果分片之前你的id是递增的, 那么分片后你就无法保证id的全局唯一性, 这时比较常见的业内方案就是UUID或者SnowFlake.

    当然如果想要排序和分页, 就需要有个id生成器去统一集中生成连续的id(参考下文).

  • 分片后的全路由

    这个是最糟糕的情况, 这种情况会让我们的查询比分片之前还要慢, 可以在自定义的分片算法中校验这种情况直接抛出异常, 然后coder们根据日志中的报错来统计这部分sql加以改写.

  • jpa级联

    如果jpa级联中包含分表, 则需要拆除这种级联关系, 以免导致上述全路由情况发生.

  • 分片后的排序&分页

    如果只是单独分页, Sharding Sphere会剔除数据不写入内存, 实际上不会导致内存的大量占用, 但如果加上排序, 那情况就不容乐观了, 官方建议通过可以保证连续性的id去加以限制.

三. 自定义复合分片算法

3.1 四种分片算法&五种分片策略

Sharding Sphere为我们提供了4种分片算法和5种分片策略(下面都是官网抄来的, 感兴趣可以在文末找到原文链接)

4种分片算法

  • 精确分片算法

    对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。

  • 范围分片算法

    对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用。

  • 复合分片算法

    对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。

  • Hint分片算法

    对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。

5种分片策略

  • 标准分片策略

    对应StandardShardingStrategy。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

  • 复合分片策略

    对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

  • 行表达式分片策略

    对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->u_id % 8 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0t_user_7

  • Hint分片策略

    对应HintShardingStrategy。通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。

  • 不分片策略

    对应NoneShardingStrategy。不分片的策略。

3.2 实现复合分片算法

以下代码根据ShardingColumnEnum这个枚举中定义的顺序进行分片, 比如该枚举中定义了三个字段名A, B, C, 那么当A不为null时, 用A分片, 当A为null但B不为null时, 用B分片, 以此类推…

详情我会用注释标注

@Slf4j
@Component
//实现复合分片算法, 泛型指定了分片后的类型是String
public class MyComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<String> 

    // 重写doSharding方法
    // 返回值: 分片后的N张表后缀
    // 参数availableTargetNames: 所有可用的表名
    // 参数shardingValue: 包含逻辑表名, 以及解析查询后的列名和对应值map
    @Override
    public Collection<String> doSharding(Collection availableTargetNames, ComplexKeysShardingValue shardingValue) 
        List<String> shardingResults = Lists.newArrayList();
        // 将查询时带的条件map拿出来
        Map<String, Collection<String>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
        // 判断查询条件带不带ShardingColumnEnum中定义的列, 不带分片键的查询统一报错
        if (!validateAndPopulateShardingValueList(shardingResults, availableTargetNames, columnNameAndShardingValuesMap)) 
            log.error("invalid params in shardingResults:", shardingValue);
            throw new IllegalArgumentException("invalid params in shardingResults!");
        
        return shardingResults;
    

    private boolean validateAndPopulateShardingValueList(List<String> shardingResults, Collection<String> tableNames, Map<String, Collection<String>> columnNameAndShardingValuesMap) 
        return Arrays.stream(ShardingColumnEnum.values())
                .map(ShardingColumnEnum::getValue)
                .anyMatch(v -> 
                    Collection<String> shardingValues = columnNameAndShardingValuesMap.get(v);
                    if (isEmpty(shardingValues) || shardingValues.contains("")) 
                        return false;
                    
                    return shardingValues.stream()
                            .anyMatch(shardingValue -> tableNames.stream()
                                    .anyMatch(tableName -> 
                                        if (tableNameEndsWithShardingValueIgnoreCase(tableName, shardingValue)) 
                                            shardingResults.add(tableName);
                                            return true;
                                        
                                        return false;
                                    ));
                );
    

    private boolean tableNameEndsWithShardingValueIgnoreCase(String tableName, String shardingValue) 
        return Optional.ofNullable(tableName)
                .map(String::toLowerCase)
                .map(t -> t.split("_"))
                .map(split -> split[split.length - 1].endsWith(shardingValue.toLowerCase()))
                .orElse(false);
    


参考

Sharding Sphere官网分片释义

Sharding Sphere分页归并

以上是关于Sharding Sphere基于复合分片键分表实战的主要内容,如果未能解决你的问题,请参考以下文章

Sharding Sphere基于复合分片键分表实战

Sharding-Sphere系列-主从配置和分库分表

Sharding-JDBC自定义复合分片算法

Sharding-JDBC自定义复合分片算法

分库分表以后如何对非Sharding键进行查询?

sharing-sphere单库分表(入门级示例)