前面谈到哨兵的作用就是无人值守运维,无非就是master宕机,slave再顶上去。但此时会有一个大问题, master挂掉后,写操作会被暂时中断,哨兵们需要投票选举新的master,选举新的leader,由leader推动故障切换流程,至此新的master才能对外提供写操作。在老master宕机到新master上位这个前期,redis服务处于一个半瘫痪状态,只能读不能写,就会导致数据流失,这个过程的持续时间可能还会因为硬件、网络等原因增长。因此承受单点高并发的master只有一个是远远不够的。所以Redis就引入了集群
Redis集群:由于数据量过大,单个Master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。
Redis集群对外是一个整体,在内部具体是哪一个实例为客户端提供服务与客户端无关。
例如,我打联通客服,人工服务,我只关心是否能够服务,并不关心对应的具体是哪位客服
对应的master宕机了,其他master也能对外提供服务,除此之外slave会上位成为master,也能对外提供服务。因此,Redis集群几乎就替代了原来的主从复制+哨兵监控机制。
一句话就是:Redis集群是一个提供在多个Redis节点间共享数据的程序集
Redis集群能干嘛?
Redis集群没有使用一致性hash,而是引入了哈希槽的概念。Redis集群中内置了16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽集群的每个节负责一部分hash槽。
举个例子,比如当前集群有3个节点,那么:
redis1负责槽位0-5460,redis2负责槽位5461-10922,redis3负责槽位10923-16383
比如冯同学需要写入一个数据,它的key为fl,经过CRC16运算后,对16384进行取余,假设余数为721,由于721这个槽位是有由redis1负责,那么这个写操作就是由redis1完成,其他redis不能进行写操作,否则会报错。读取操作也是由redis1负责
大致意思:
集群的密钥空间被划分为16384个槽位,有效地设置了集群规模的上限为16384个主节点(然而,节点的最大值建议是大约1000个节点)。
后续会讲到为什么只有16384个槽位,为什么建议最大值为1000个节点
使用Redis集群时我们会将存储的数据分散到多台redis机器上,这称为分片。简言之,集群中的每个Redis实例都被认为是整个数据的一个分片。
如何找到给定key的分片
这个问题的答案在前面已经给出,但是我还是要再讲一遍,加深印象。
为了找到给定key的分片,我们对key进行CRC16(key)算法处理并通过对总分片数量取模。然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来读取特定key的位置。
采用槽位和分片有两大优势:方便扩容和数据分派查找
这种结构很容易添加或者删除节点比如如果我想新添加个节点D,我需要将节点A, B, C中的部分槽转移到D上。如果我想移除节点A,需要将A中的槽转移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态
哈希取余分区
2亿条记录就是2亿个k,v, 我们单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:hash(key) % N个机器台数,计算出哈希值,用来决定数据映射到哪一 个节点上。
优点:
简单粗暴,直接有效,只需要预估好数据规划好节点,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求 ( 并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点:
原来规划好的节点,进行扩容或者缩容就比较麻烦了额,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化:Hash(key)/3会变成Hash(key)/?。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。某个redis机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌。
一致性哈希
我之前写了一篇博客,可以了解一下,这里就不多赘述----》一致性哈希
哈希槽分区
哈希槽分区的相关概念已经讲过,这里不多赘述。
那么为什么会采用哈希槽分区算法,而不采用其他两种方法呢?
哈希取余的缺点太过明显,就不用说了。而一致性哈希感觉也没啥大毛病,为啥不用一致性哈希算法呢?
一致性哈希算法可以帮助在Redis节点之间平衡数据负载。但它有一个缺点,就是当节点增加或删除时,可能需要重新分配许多键值对,这会导致大量的数据迁移,带来额外的网络开销和延迟。此外,一致性哈希算法在节点故障时也可能导致数据不平衡。
哈希槽架构则更加直接。Redis把数据分成固定数量的哈希槽,每个节点负责一部分哈希槽。当节点增加或删除时,只需将它们的哈希槽重新分配给其他节点即可,这个过程不会涉及到大量的数据迁移。这使得Redis的扩展更加容易和可控,而且在节点故障时也更加稳定。此外,哈希槽架构也更加容易进行备份和恢复操作。
总的来说,哈希槽架构相对于一致性哈希算法来说更加简单、可控、易于扩展和稳定,这也是Redis采用哈希槽架构的原因。
CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。换句话说值是分布在0~65535之间,有更大的65536不用为什么只用16384就够?作者在做mod运算的时候,为什么不mod65536,而选择mod16384?HASH_ SLOT = CRC16(key) mod 65536为什么没启用?
看看作者的原话:
原话翻译:
正常的心跳包携带一个节点的完整配置,可以用幂等的方式替换旧的配置以更新旧的配置。这意味着它们包含一个节点的插槽配置,以原始形式,使用16k插槽使用2k空间,但使用65k插槽将使用令人难以忍受的8k空间。
同时,由于其他设计上的权衡,Redis集群不太可能扩展到1000多个主节点。
因此,16k是正确的范围,可以确保每个master有足够的插槽,最多为1000个maters,但这个数字足够小,可以很容易地将插槽配置为原始位图。请注意,在较小的集群中,位图将很难压缩,因为当N很小时,位图会有槽/N位,这是一个很大的百分比。
看不懂没关系:作者的意思可以总结为以下三点:
(1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时, 这块的大小是: 65536+8+1024=8kb
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时, 这块的大小是: 16384+8+1024=2kb
因为每秒钟,redis 节点需要发送一定 数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
(2)redis的集群主节点数量基本不可能超过1000个
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群, 16384个槽位够用了。没有必要拓展到65536个。
(3)槽位越小,节点少的情况下,压缩比高, 容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率 slots / N很高的话(N表示节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
Redis集群不保证强一致性, 这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令
例如:当客户端写了三条数据,此时master也写完成,准备给slave同步数据时宕机了,这三条数据就丢失了,slave上位成为master,对外提供服务,此时客户端查看数据时,并没有这三条数据,因此造成了数据不一致
本次案例我在一台云服务器完成,但实际生产上肯定是搭建在多台云服务器上
整体架构
新建6个独立的redis实列服务
每个.conf文件内容都为,只需要变动6381即可:
bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/root/myredis/cluster/c1uster6381.log"
pidfile /root/myredis/clustercluster6381.pid
dir /root/myredis/cluster
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendon1y6381.aof"
requirepass 123456
masterauth 123456
cluster-enabled yes #打开集群
Cluster-config-file nodes-6381.conf #集群的配置文件叫nodes-6381.conf
cluster-node-timeout 5000 #集群之间的超时时间
在复制上述配置时,后面的中文信息一定要删掉,否则在启动redis实例时,会失败
启动6台redis主机实例
使用命令构建主从关系
redis-cli -a fl12345.0 --cluster create --cluster-replicas 1 ip:6381 ip:6382 ip:6383 ip:6384 ip:6385 ip:6386
解释:–cluster-replicas 1表示为每个master创建一个slave节点
查看是否生成对应的log和nodes.conf
链接进入6381作为切入点,查看并检验集群状
在前面的整体架构图中,主机6381下面是从机6382,但是现在下面却是从机6384,以实际分配为准
可以用命令 cluster nodes 查看集群之间的基本关系
也可以用命令 cluster info 查看信息
验证是否能进行读写操作
为什么插入 k2 v2就行,而插入k1 v1 就失败呢?
因为k1和k2映射的槽位不同,需要到负责对应槽位的redis操作才能成功,也就是说需要路由到位
可以看到该操作被重定向到了主机6383
用命令 cluster keyslot key 查看key属于哪个槽位
主机6381停机,从机6384是否能成功上位成为master
查看集群关系
6381重新启动,是否会再次成为master呢?
答案是并不会,会以slave的形式回归,它会成为主机6384的slave
在上述过程中,6381从新上位,成了6384的slave,但是我们还是想让6381成为6384的master,如何做?可以使用命令 cluster failover 手动调整节点从属关系
主从扩容
新建6387、 6388两个服务实例配置文件+新建后启动,这里的操作和前面的一样,就不多赘述
将新增的6387节点(空槽号)作为master节点加入原集群
redis-cli -a密码 --cluster add-node 自实际IP地址:6387 自己实际IP地址:6381
6387就是将要作为master新增节点
使用命令redis-cli -a 123456 --cluster check ip:6381 产看一下对应的集群信息,以及槽位信息
重新分配槽号
命令:redis-cli -a 密码 --cluster reshard IP地址:端口号
这里选择分配4096个槽位,主要原因是一共有16384个槽位,4台主机,16384 / 4 = 4096
再次检测集群信息,以及槽位信息
为什么6387是3个新的区间,以前的还是连续?
重新分配成本太高,所以之前的三台主句各自匀出来一部分, 从6381/6382/6383三个旧节点分别匀出1364个坑位给新节点6387
为主机6387分配从机6388
命令:redis-cli -a 123456 --cluster add-node ip:6388 ip:6387 --cluster-slave --cluster-master-id master_id
第三次检测集群信息,以及槽位信息
主从缩容
从集群中将4号从节点6388删除
命令:redis-cli -a 密码 --cluster del-node ip:从机端口 从机6388节点ID
检测集群信息,查看从机6388是否被删除
将6387的槽号清空,重新分配, 本例将清出来的槽号都给6381
命令:redis-cli -a 密码 --cluster reshard ip:端口号
第二次检查集群信息,查看槽号是否重新分配
4096个槽位都指给6381,它变成了8192个槽位,相当于全部都给6381了,不然要输入3次
将主机6387删除
命令:redis-cli -a 密码 --cluster del-node ip:端口号 6387主机ID
第三次检查集群信息,查看主机6387是否被删除
Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽。到底CRC16是如何调用的?
看一下cluster.c源码
先调用keyHashSlot,再调用crc16
集群是否完成才能对外提供服务
cluster-require -full coverage
默认是yes,现在集群架构是3主3从的redis cluster由3个master平分16384个slot,每个master的小集群负责1/3的slot,对应一部分数据。cluster-require-full-coverage:默认值 yes ,即需要集群完整性,方可对外提供服务通常情况,如果这3个小集群中,任何一个(1主1从)挂了,你这个集群对外可提供的数据只有2/3了,整个集群是不完整的, redis默认在这种情况下,是不会对外提供服务的。
如果你的诉求是,集群不完整的话也需要对外提供服务,需要将该参数设置为no,这样的话你挂了的那个小集群是不行了,但是其他的小集群
仍然可以对外提供服务。
不在同一个slot槽位下的键值无法使用mset、mget等多键操作,因为请求没有对应到相应的槽位
不在同一个slot槽位下的多键操作支持不好,通识占位符登场
可以通过{}来定义同一 个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去
{ }里面的z可以换成任何标识符