-
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集
-
比如我们现在要存储网站的用户登录信息
-
假设有10万个用户在该网站登录, 如果要存储这些数据需要的空间一定是较大的, 单机肯定是不行的, 必须使用分布式存储, 比分布10台机器, 每台机器存储1万个用户
-
并且我们还需要考虑,如何将这10台机器与我们的数据建立起映射关系呢? 换句话来说就是,我们如何确定哪些数据应该放在哪个机器呢?这时就需要用到哈希算法。
简单哈希
-
我们可以采用除留余数法来完成一个映射,key值为用户账号,余数为机器数量,得到的结果就是应该存储的机器的编号。这样我们将数据放入指定机器中,使用时再根据机器号进入对应的机器进行增删查改即可。
-
机器号 = hash(账号) % 机器数量
-
但是这个方法存在着一个致命的缺陷,随着用户量不断增多或者用户信息增加,10台机器就会不够用,比如网站日益强大此时有了11万用户,此时就需要将机器扩容至11台。
-
当进行扩容后,由于机器数量发生变化,数据的映射关系也会变化,机器号 = hash(账号) % 机器数量 我们就需要进行rehash来将数据重新映射到正确的位置上。
-
但是问题来了,这10台机器的数据如果需要进行重新映射,花费的时间几乎是不可想象的,我们不可能说为了迁移数据而让服务器宕机数月之久,所以这种方法是不可能行得通的。
-
为了弥补上一种方法的,就引入了一致性哈希算法。
-
上面一种方法的主要缺陷就是由于扩容后rehash带来的数据大量迁移问题。
-
为了解决上述问题,一致性哈希将哈希构造成一个0~2^32-1的环形结构,并将余数从原来的机器数量修改值为整型最大值(也可以是比这个更大的)。因为这个数据足够大,所以不需要考虑因为机器数增加导致的完全rehash问题。
-
机器号 = hash(账号) % 2^32
- 我们将环中的某一区间去映射到某台服务器,让这台服务器负责这个区间的管理,这样就能让这10台服务器来切分这个闭环结构
- 当我们要查询某个数据的时候,根据哈希函数算出的映射位置来找到包含该位置的那个区间所对应的服务器,然后在那个服务器中进行操作即可
- 如果原先的服务器不够用了,此时增加1个服务器,也不需要像之前一样对所有机器的数据进行迁移,我们只需要迁移负载重的机器即可, 将原先这台机器负责的区间进行划分一人再负责一半区间, 然后将原先服务器的数据进行rehash即可
-
从这里我们可以看到,一致性哈希将服务器数据的整体迁移变成了高负载服务器的部分迁移,大大提高了效率以及稳定性。
-
所以一致性哈希就是一个大范围的闭环,由于除数过大,我们也不需要因为由于除数 数量的增加导致全体rehash。并且映射关系变味了数据区间——机器,如果要增加机器,就只需要改变映射范围,并将区间中的小部分数据进行迁移,大大的提高了效率。
-
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响
-
但一致性哈希分区存在问题:
- 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案, 此时就会出现这种情况,部分节点数据过少,而部分节点数据过多,此时的数据大量集中在一个节点上,因为节点分布不均匀而导致数据倾斜问题。
- 会有人想, 那就可以考虑在不增加服务器的基础上多增加几个节点,所以为了解决这问题,一致性哈希又引入了虚拟节点。对每个服务节点进行多次哈希映射,每个映射的位置都会放置该服务节点,成为虚拟节点。例如下图,就分别将NodeA和NodeB分成了三个虚拟节点。我们不需要改变数据定位的算法,只需要将虚拟节点与服务节点进行映射,将定位到虚拟节点NodeX #1、#2、#3的节点再定位回服务节点即可。
- 因为redis是一个高速的存储服务器 , 即使使用一致性hash在扩容的时候还是会导致一个节点的部分时间不可用
- **所以采用虚拟节点要增加或者减少服务器的时候就可以可以将原来的虚拟节点变成一个真的节点, 将原来由这个虚拟节点映射过来的数据直接复制到新节点上, 就省去了rehash的操作
-
正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改造
-
其实本质上虚拟槽中的槽就是大量的虚拟节点的抽象化, 将原来的虚拟节点变成一个槽, redis内置是有16383个槽也就是有16383个虚拟节点
-
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围 一般远远大于节点数,比如Redis Cluster槽范围是0~16383
-
如下图所示:当前集群有5个节点,每个节点平均大约负责3276个槽
-
由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区, 所以redis使用的就是虚拟槽分区
-
采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据,如下图所示
- Redis虚拟槽分区的特点:
-
解耦数据和节点之间直接关系, 一致性hash计算方式是key & 节点个数, 映射的结果和节点个数有关系, 但是使用hash槽计算方式是 CRC16(key) % 槽的个数, 所以就解耦了数据和节点的关系
-
使用哈希槽的好处就在于可以方便的添加或移除节点。
-
当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
-
当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
-
在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务甚至不用停到任何一个节点的服务。对应槽的采用直接复制数据过去显然比rehash快很多
- 节点自身维护槽的映射关系, 不需要客户端和代理服务器进行维护处理
- Redis集群相对单机在功能上存在一些限制
-
key批量操作支持有限。如mset(批量赋值)、mget(批量取值),目前只支持具有相同slot值的 key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持
-
key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能
-
key作为数据分区的最小粒度,因此不能将一个大的键值对象如 hash、list等映射到不同的节点
-
Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。
-
当我们启动6个节点,但每个节点彼此并不知道对方的存在,通过节点握手让6个节点彼此建立联系从而组成一个集群
-
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信, 达到感知对方的过程
-
节点握手是集群彼此通信的第一步,由客户端发起下面的命令
cluster meet {ip} {port}
- 上面执行命令之后让节点6379和6380节点进行握手通信。cluster meet命令是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信,如下图所示:
1)节点6379本地创建6380节点信息对象,并发送meet消息
2)节点6380接受到meet消息后,保存6379节点信息并回复pong消息
3)之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信
- 这里的meet、ping、pong消息是Gossip协议通信的载体,之后的节点通信部分做进一步介绍,它的主要作用是节点彼此交换状态数据信息。6379和6380节点通过meet命令彼此建立通信之后,集群结构如下图所示:
- 下面分别执行meet命令让其他节点加入到集群中,我们只需要在集群内任意节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内通过ping pong传播,这样其他节点会自动发现新节点并发起握手流程:
分配槽
- Redis集群把所有的数据映射到16384个槽中。每个key会映射为一个固定的槽,只有当节点分配了槽,才能响应和这些槽关联的键命令。通过
cluster addslots
命令为节点分配槽。这里利用bash特性批量设置槽(slots)
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0…5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462…10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923…16383}
-
作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。集群模式下,Reids节点角色分为主节点和从节点。首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。
-
Redis集群模式下的主从复制使用了之前介绍的Redis复制流程,依然支持全量和部分复制和增量复制
-
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息
-
Redis集群采用P2P的Gossip(流言)协议, Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节 点都会知道集群完整的信息,这种方式类似流言传播,如下图所示:
通信过程说明:
1)集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信
2)每个节点在固定周期内通过特定规则选择几个节点发送ping消息
3)接收到ping消息的节点用pong消息作为响应
-
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。
-
当节点出故障、新节点加入、主从角色变化、槽信息 变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的 节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的
Gossip消息
-
Gossip协议的主要职责就是信息交换。为了实现信息大一统
-
常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息等
-
meet消息:用于通知新节点加入。消息发送者通知接收者加到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong消息交换
-
ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其 他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据。
-
pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内 广播自身的pong消息来通知整个集群对自身状态进行更新
-
fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
- Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容,如下图所示:
集群扩容
- 扩容是分布式存储最常见的需求,Redis集群扩容操作可分为如下步骤:
- 准备新节点
启动后的新节点作为孤儿节点运行,并没有其他节点与之通信
- 加入集群
新节点依然采用cluster meet命令加入到现有集群中。在集群内任意节点执行cluster meet命令让6385和6386节点加入进来
redis-cli -p 6379 cluster meet 127.0.0.1 6385
redis-cli -p 6379 cluster meet 127.0.0.1 6386
- 集群内新旧节点经过一段时间的ping/pong消息通信之后,所有节点会发现新节点并将它们的状态保存到本地
- 迁移槽和数据
分享
这次面试我也做了一些总结,确实还有很多要学的东西。相关面试题也做了整理,可以分享给大家,了解一下面试真题,想进大厂的或者想跳槽的小伙伴不妨好好利用时间来学习。学习的脚步一定不能停止!
Spring Cloud实战
Spring Boot实战
面试题整理(性能优化+微服务+并发编程+开源框架+分布式)
新节点并将它们的状态保存到本地
- 迁移槽和数据
分享
这次面试我也做了一些总结,确实还有很多要学的东西。相关面试题也做了整理,可以分享给大家,了解一下面试真题,想进大厂的或者想跳槽的小伙伴不妨好好利用时间来学习。学习的脚步一定不能停止!
[外链图片转存中…(img-5V3aqFkp-1714440024455)]
Spring Cloud实战
[外链图片转存中…(img-KjkmXrzF-1714440024456)]
Spring Boot实战
[外链图片转存中…(img-ersq8uMj-1714440024456)]
面试题整理(性能优化+微服务+并发编程+开源框架+分布式)