问题引入
最近一个业务系统中,因为数据量很大,经过技术选型,综合权衡选择了sharding-Jdbc,本文主要讨论的是分库分表的表达式
我们有一个批次总表A,还有一个明细表B,我们需要对明细表B进行水平拆分,考虑系统数据的可扩展性和mysql的负载,我们打算针对B表进行分表,分4个数据库,每个数据库64张表,使用A表的主键批次id【fnQpid】来作为分片键。
问题演进
第一版本
一开始选用了一个最简单的hash路由策略,如下:
分库策略为
fnQpid % 4
分表策略为
fnQpid % 64
乍一看,没啥问题,仔细琢磨后发现了一个问题,这样分会导致数据分布不均衡,为啥会导致数据不均衡呢?
假设某一个fnQpid的数据分在了0库的1分表,那么这个fnQpid需要满足什么条件呢?
第一,分在了0库需要满足,fnQpid=4m 【m为整数】 ====》可以推测出fnQpid为偶数
第二,分在1分表需要满足,fnQpid=64n+1 【n为整数】====》可以推测出fnQpid为奇数
毫无疑问,这样的fnQpid是不存在的,也就是某一个fnQpid的数据分在了0库的1分表这个假设不成立,也就是这样分库分表会导致数据分布不均衡。
第二版本
发现上面的问题后,优化了一版分库分表的算法
分库策略
(fnQpid % 256)/64
分表策略
(fnQpid % 256) % 64
简单介绍一下这个方案的思路
- 先计算总的逻辑表的数量,也就是4*64=256
- 然后计算fnQpid % 256的值,这个值的范围是[0,255],把这256个数据当成是一个整体来看,计算其所在的数据库(fnQpid % 256)/64,该值的范围是[0,3]
- 最后计算所在分表(fnQpid % 256) % 64,该值的数据范围是[0,63]
按照这样的分库分表方式,不会出现第一种情况(有些表永远不会有数据出现),但是这样的路由策略有一个美中不足,如图
当批次号连续自增的时候,明细表的数据分布是这样的,前256个批次的数据在0库,接着在1库,接着在2库,最后在3库,再接着又回到0库,如此循环往复。
若每个批次有1000条数据,那么在短时间内,就会有256000的数据落在同一个数据库,这个瞬时数据的写入、查询都是一个不小的压力。可以说这个方案,数据物理上是分布均衡的,但是在短时间内,mysql单个数据库的负载是不均衡的。
第三版本
考虑到上面两种问题后,继续优化
分库策略为
fnQpid % 4
分表策略为
(fnQpid >> 2) % 64
简单介绍下这个方案
这个方案是在方案一的基础上,进行了分表策略的优化。
一开始是直接用fnQpid % 64,这样有问题是fnQpid的最后两位二进制已经参与了数据库(fnQpid % 4)的计算了,再参与分表的计算,就会互相影响。新的方案是,先踢掉最后两位二进制(fnQpid >> 2),再参与分表计算,数据流入如下图
可以看到,数据是按照顺序从0库到3库先填充满所有的0表,接着填充满所有的1表…,填充所有的63表,再填充满所有的0表,如此循环往复。当瞬时间有大量数据写入的时候,数据是分散在不同的数据库的,因此解决了方案二(数据库负载不均衡)的问题了
问题总结
为什么分表的数量是64呢?为什么不是100呢?
针对这个问题,大家可以思考下为什么HashMap的初始容量要设置成16(2的4次幂),redisCluser的哈希槽为什么是16384(2的14次幂)?
我个人认为,是为了提升速度。因为无论是HashMap还是redisCluser,都需要频繁的根据key来计算数组的index,这个操作是hash(key)%16、hash(key)%16384,这样做就差点意思。
看过hashMap源码的同学都知道,源代码上使用了hash(key)&15 来计算index,也就是hash(key)&15等价于hash(key)%16,延展下,
若n为2的整数次幂时 m % n 等价于 m & (n-1)
因此,我们的分库分表为了提速,分表数量选择了2的整数次幂。最终的分库分表的表达式是这样
分库策略为
fnQpid & 3
分表策略为
(fnQpid >> 2) & 63
针对分库分表的策略,大家还遇到过什么坑吗?欢迎评论区留言!