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_0
到t_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基于复合分片键分表实战的主要内容,如果未能解决你的问题,请参考以下文章