这种实现最大的特点就是简单并且查询友好,因为所有相关的数据都按照时间范围落在一个分片里。并且因为 B端系统低并发的特性,这样查询不会带来什么效率问题。
但是如果是C端系统就不行了,按照时间范围拆分会带来热点数据倾斜,可能即使分了很片,数据库依然频频告警。
所以 C 端系统一般不会选择时间范围去拆分,而是选择一个合适的算法,不仅是让数据均匀去分布,而且需要让热点数据去均匀分布。这样用户的请求才能均匀去分布到每个机器上,发挥出分库分表的优势。
数据映射表
均匀分配数据的关键就在于知道有多少数据分配在了哪些库里,那我们专门用一个表记录这些 Sharding Key 对应的分片不就可以了吗?数据映射表方案是这样的,查询的时候先使用 Sharding Key 去映射表查询对应的分片信息,然后再根据分片信息去指定分片查询。
这样的好处是:数据分配灵活,可以人为掌控,想怎么分就怎么分,并且后期扩容方便,我想把哪些数据迁移到新数据库直接调整映射表就好了。
缺点也显而易见:这样的查询需要多一次数据库连接的过程,当然这个问题可以通过把热点映射 Key 加入到缓存中来进行优化。但是如果映射表数据过多,而我们又需要对映射表频繁写入和查询,映射表本身就容易成为性能瓶颈。
图片
ID取模算法
ID取模算法大家应该都能想到。我需要分N个库,把用户 id 对N直接取模就可以了,结果就等于分库号。比如用户id是2023007,限制有10个分库,那么2023007%10=7,7就是分库号。虽然简单,但是对于大多数场景它完全可以胜任,因为大多数系统并没有非常夸张的数据量。
相比第一种范围分片,它的数据均匀程度通常要好很多。但是这是有前提条件的,他非常依赖ID的自增连续性,要求尾号相对连续。比如尾号不能和性别、地域等因素有关系,否则会影响数据再分片中的分布情况。
ID取模:
public class ShardingUtil {
// 计算数据表编号
public static int shard(int id, int tableCount) {
return id % tableCount;
}
}
Hash分片算法
Hash 分片算法也叫 Hash 取模算法,他跟ID 取模算法类似,最终都是对分片数量 N 取模,但是不同的是它会在取模之前先对 Sharding Key 做一次Hash计算。示例如下:
public class ShardingUtil {
// 计算哈希值
public static int hash(String key) {
int hash = 0;
for (int i = 0; i < key.length(); i++) {
hash = hash * 31 + key.charAt(i);
}
return hash;
}
// 计算数据表编号
public static int shard(int hash, int tableCount) {
return (hash & Integer.MAX_VALUE) % tableCount;
}
}
它的好处是,相比简单的直接ID取模,多了一步Hash,让数据分布更为均匀,且实现依旧简单。但是缺点依旧存在,扩容不方便的问题并没有得到解决,需要以2的倍数扩容,扩容时候最少需要迁移50%的数据到新分片上。
一致性Hash算法
一致性哈希算法是用于解决分布式系统中的数据分片和负载均衡问题的一种算法。一致性哈希算法的基本思想是将所有的数据和服务器都映射到一个固定大小的哈希环上,通过对数据的哈希值进行计算,将其映射到环上的某个位置,然后选择离这个位置最近的服务器作为数据的存储位置。
环的大小是2^32-1,这样就可以保证每一个ip地址都可以在环上获得一个映射位置,并且这个足够大的数字可以提供更高的数据均匀度。查找数据时key落在某个点后,继续向前遇到的第一个Node就是要查找的分片。在添加或删除服务器时,只会影响哈希环上与该服务器相邻的一小段数据的存储位置,而不会影响整个系统的数据分布。
图片
在分库分表中,假设我们有N个数据库或数据表,我们可以将它们映射到一个固定大小的哈希环上,然后通过数据库的IP地址计算数据的哈希值,将其映射到环上的某个位置,并选择离该位置最近的数据库或数据表作为数据的存储位置。
但是标准的一致性Hash通过 hash(数据库IP地址) % 2^32 计算数据库在Hash环中的位置,如果分片数量较少,会造成数据库节点在Hash环节点分布不均匀,最终导致数据分布不均匀。并且现在大都使用云环境,节点的IP地址很可能会发生变化,从而导在hash环位置的变化。解决这个问题有下面几种办法:
- 对于IP位置变更的问题,我们可以不采用IP去Hash,而使用分片的逻辑号,比如Node1、Node2。
- 对于Hash偏斜问题,我们可以通过增加虚拟节点来解,认为去将数据调配。
但是通过上述优化,我觉得这样的一致性Hash算法还是不够简单。首先是DB数量不够多的情况下,增加虚拟节点的问题,还得多去维护虚拟节点。
这里有两种解决思路:
- 对DB计算Hash值的算法进行改写,直接根据逻辑编号指定在Hash环位置的映射位置,比如指定Node1、Node2分别在Hash环的起点和中点。
- 其实可以参考Redis cluster 采用的Hash槽算法的思路。我们直接将Hash环简化为4096或者2048个槽位,将固定范围的槽位分配到对应节点上。
这样,数据的均匀分布情况得到进一步的优化。
示例如下:
public static final int SHARDING_COUNT = 4;
// 计算数据表编号
public static int shard(Long id) {
return (Math.abs(id.hashCode()) % 2048) / (2048 / SHARDING_COUNT);
}
一致性hash分片算法的主要优点如下:
- 数据分布均匀,且可以人为调控。
- 扩容方便,没有2倍限制,只需要迁移1/N的数据。
主要缺点:
- 相比于其他算法,他的实现更为复杂。
- 有分布不均匀的问题,但是可以通过虚拟节点解决,或者指定槽位。
它能够帮助分布式系统实现数据的分片和负载均衡,提高系统的可伸缩性和可用性。
Redis Cluster采用的是一种基于哈希槽的分片机制,该机制是一致性哈希算法的一种变体,它将整个哈希环分成固定数量的槽,每个节点负责一定数量的槽。
具体来说,Redis Cluster将整个哈希环分成16384个槽,每个节点负责其中的一部分槽,每个key通过哈希函数计算出一个哈希值,并映射到某个槽中,然后根据槽和节点之间的映射关系,将该key存储到对应的节点中。在Redis Cluster中,每个节点都维护了一个槽的分配表,记录了每个槽被分配给了哪个节点。
相对于传统的一致性哈希算法,Redis Cluster采用哈希槽算法的优势在于:
- 算法简单:将哈希环分成固定数量的槽,每个节点负责一定数量的槽,可以简化分片算法的实现。
- 可扩展性好:增加或删除节点时,只需要重新分配一部分槽即可,不需要像一致性哈希算法那样重新映射所有的key,可以有效地减少数据迁移量。
- 灵活性高:可以根据实际需要调整槽的数量和节点的分配情况,以满足不同的应用场景。
总结
最后用一张表总结一下
分片算法 | 优势 | 缺点 |
范围分片 | 实现简单,扩容方便。 适合数据量大但并发量不高的B端系统 | 容易产生热点数据问题,访问不均匀。不适合高访问量的2C系统 |
数据映射表 | 数据分配灵活,可以人为掌控,并且后期扩容方便。 | 查询时候至少需要两次数据库连接的过程,映射表容易成为热点和性能瓶颈点。 |
ID取模算法 | 实现简单,适用大多场景 | 对于业务ID的连续性过于依赖,ID尾号的自增和分布情况会影响数据分布,容易存在不均匀问题.随着业务增长扩容不方便。 |
哈希分片算法(哈希取模) | 实现简单,相对来说数据分布更均匀 | 同样,随着业务增长扩容不方便,需要以2的倍数扩容,最少迁移1/2的旧数据。(联想HashMap) |
一致性哈希算法 | 数据分布均匀,并且可以添加虚拟节点进一步调控数据的均匀程度。扩容方便,并且在扩容时候只需要迁移少量数据。没有2倍限制,增加一个节点只需要迁移最多1/n数据。 | 算法实现复杂,也会有节点分布不均匀的情况,但是可以通过虚拟节点解决,或者利用Hash槽人工分配(联想Redis Cluster)。 |
最后,欢迎大家提问和交流。