首先这么多数据,单机单台服务器肯定是不行的,要采用分布式存储的方案,那我们用Redis应该如何落地这个方案呢?
既然选用分布式存储,那么服务器肯定是有多台的,我们首先要思考的问题就是以哪种方式判断数据该存放到哪台机器上,使存储的数据尽量均匀的分布在各个机器上,并且要考虑当某个节点宕机了我们应该如何应对。
接下来我们就讨论几种设计方案,并分析他们的原理和宕机时的情况。
一、哈希取余法
2亿条记录就是2亿个 k,v,单机肯定是无法存储的,这里我们假设有三台机器,用户每次读写操作都是根据公式 :hash(key) / N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。
这种方式比较简单,直接得到key的hash值,然后对机器数量进行取余,即可得到要存的位置。
优点
简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台..,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台机器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点
扩缩容时比较麻烦,不管缩容还是扩容,都会比较麻烦,因为一旦机器数量发生了变化,原本key的映射关系就要全部重新计算,如果需要弹性扩容或者故障停机的情况下,原来的取模公式就会发生变化,这时取余的结果就会发生很大变化,所以会导致全部数据要重新洗牌。
二、一致性哈希算法
这个算法在1997年就被提出了,设计的目标就是为了解决分布式缓存数据变动和映射的问题。当服务器的个数发生变动时,尽量减少影响客户端到服务器的映射关系。
2.1算法原理
构建一致性哈希环
一致性哈希算法必然有个hash函数,并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间 [0, 2 ^ 32 -1],这是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首尾相连(0 = 2^32),这样让它在逻辑上形成了一个环形空间。它也是按照使用取模的方法,上一种方式的节点取模法是对节点数量进行取模。而一致性哈希算法是对2^32取模,简单来说,一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,比如某哈希函数H的值空间为 0 ~ 2^32-1 (即哈希值是一个32位无符号整形),整个哈希环如下图:整个空间按照顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2,3,4、... 直到2^32-1,也就是说0点左侧的第一个点代表2^32-1,0和2^32-1在零点中方向重合,我们把这个有2^32个点组成的圆环称为哈希环。
服务器IP节点映射
将集群中各个机器的IP映射到环上的某一个位置,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定自己在环上的位置。假如4个结点NodeA、B、C、D,经过IP地址的哈希函数计算(hash(ip)),使用IP地址哈希后在环空间的位置如下:
Key落到服务器的落键规则
当我们需要存储kv键值对时,首先计算出key的hash值,hash(key),将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
比如我们存储 (K1,V1)键值对,经过哈希计算后,确定K1在圆环上的位置如下:
根据一致性Hash算法可知,K1顺时针方向“行走”遇到的第一个节点是NodeB,所以键值对(K1,V1)存储在NodeB上。
2.2优点
容错性
假设NodeB宕机了,NodeA、C、D不会受到影响,只有NodeB被重定位到NodeC,一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器与在环上的上一个服务器之间的数据,其他不会受到影响,简单说就是NodeB挂了,受影响的只有NodeA和NodeB之间的数据,并且后续原本要存到NodeB上的这些数据会转移到NodeC进行存储。
扩展性
假设现在数据量增加了,需要再添加一个节点NodeX,NodeX的位置在NodeA和NodeB之间,那么受到影响的数据也就是 NodeA到NodeX之间的数据,重新把NodeA到NodeX之间的数据录入到NodeX上即可,不会导致hash取余全部数据都要重新洗牌。
2.3缺点
数据倾斜问题
如果节点太少的时候,很容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中在某一个节点上)问题,比如就两个节点A、B。
三、哈希槽
为了解决一致性哈希算法存在的数据分配均匀问题,推出了哈希槽。哈希槽实质上就是一个数组,数组[0,2^14 - 1]形成hash slot 空间。在数据和节点之间又加入了一层,把这层称为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里面放的就是数据。
槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。
Redis集群并没有使用一致性hash而是引入了哈希槽的概念。一个集群只能有16384个槽,这些槽会分配给集群中的所有主节点,分配策略没有要求。可以指定哪些编号的槽分配给哪个主节点。集群会记录节点和槽位的对应关系。
存入数据的时候,先对key求哈希值,然后对16384取余,余数是几,key就落入到哪个槽位上,slot = CRC16(key) % 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动的问题就解决了。
如果某个节点挂掉了,Redis集群会自动把挂掉的这台机器对应的槽位均匀地分配到集群中正常的节点中。
总结
哈希取余法适用于数据量不多,并且节点数量固定的场景,但是一旦有节点宕机,数据和节点的映射关系就要全部重新计算。
一致性哈希算法虽然解决了哈希取余法中节点宕机产生的问题,但是又带来了数据倾斜的问题,业内的解决方案是采用虚拟节点的方式解决,这种方式实际操作起来比较复杂。
哈希槽是Redis官方使用的集群解决方案,因为槽位是固定这么多的,如果有节点宕机,只需要移动槽位到其他节点即可,比较方便。
所以如果要存1~2亿条缓存数据,可以直接采用Redis集群的方式。