分库分表分片键设计:基因法
分库分表
随着业务发展,单库单表已经不能支撑数据量的增长,此时利用分库分表对数据进行横向拆分。
为何要分表?当单表数据量过大时,会导致索引效率降低,至于这个数据量何时过大,网上通常说1千万,但是还是要结合实际情况来看, 之前遇到过单表5亿条数据的订单单表,也不影响业务使用。
为何要分库(实例)?当数据量过大、数据库连接负载大时,单实例的IO性能有限制,因此拆分实例提升IO性能,且分库能可支持并行查询。
分库分表能够用来应对数据量的不断增长,但任何技术都不是银弹,分库分表有哪些限制呢?
从查询场景来看,由于对全量数据做了横向拆分,且数据位于不同实例,以下操作均不能直接应用在 分库分表中,比如:范围查询、排序、分页、跨库join、非分片键查询。
从写入场景来看,由于对库表进行了分片,跨库写入不支持本地事务,且写入哪一个分片也是一个问题。
此外分库分表根据什么进行划分,依据时间?依据数据量?依据某个字段?具体场景要具体分析,通常在 业务上根据业务唯一键进行分片。
针对以上查询场景遇到的问题,通常查询异构数据源存储数据副本来完成,如将MySQL数据同步到ElasticSearch(ES), ES天生支持滚动索引+索引别名,可以支持数据量的持续增长,且易于对冷热数据做区分处理。比如我们在MySQL的订单表写入后将数据同步到 ES,同步方式包括双写/订阅binlog,同步过程不是原子的,因此需要有补偿机制和数据一致性核对保障,ES中订单表按创建时间每个月自动创建新索引,通过索引别名在业务代码中进行操作。
ES能满足分库分表场景下的大部分查询逻辑,如范围查询、排序、分页、非分片键查询、跨库join(可通过ES生成宽表间接实现)。
那写入场景呢,我们如何选择分片?如何生成分片键?
基因法
如何选择分片,通常对业务id进行取模来定位分片,业务id生成使用基因法,先来看下原理。
如果我们对一个10
进制数按10
取模,余数由该数的最后1
位决定, 例如 109 % 10 = 9 % 10 = 9,383 % 10 = 3
。
推广一下:一个 10
进制数按 10^n
取模,余数由该数的最后 n
位决定。
再推广一下:一个 m
进制数按 m^n
取模,余数由该数的最后 n
位决定。
同理:一个二进制的值,按2^n
取模,也是最后n
位决定取模结果。
比如我们将订单号orderNo作为分片键,我们有16个库(一般设置为2的n
次方,可转化为2进制数, 即16d = 1111b
),那么hash后的数据库分片编号为 shardNo = orderNo % 16 = orderNo取二进制最后4位 % 16
。
如果我们将orderNo的二进制最后4位利用起来,将userId的二进制最后四位注入orderNo基因中,那么不仅可以根据orderNo确定分片编号,也可以根据userId确定,即orderNo和userId共享“分片基因”。
由于取模操作和末尾位数相关,数据取模后应均匀分布到分片上,避免数据倾斜,而userId通常随机生成,能够保证均匀分布。通常利用结合分布式ID生成算法如snowflake来生成userId/orderNo,而snowflake最后几位是为并发保留的自增序列号,通常情况下都是0,这将导致数据倾斜,所以基于snowflake生成分库分表场景下的分布式ID还是需要改造一下, 比如并发保留的自增序列号前移,最后几位改成随机数。