集群的引入
上一章我们学习了哨兵机制,哨兵有一个缺陷,就是不能保证数据不丢失。
如何解决?
答:使用Redis的集群
1 Redis集群(cluster)简介
1.1 什么是Redis 集群
数据量过大时,单个Master复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展,每个复制集只负责存储整个数据集的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。
通俗的解释就是 多个redis主从复制服务的组合(集合),每个服务都只负责整个数据集的一部分,并且互相都能共享数据。
1.2 Redis集群的功能
- Redis集群支持多个Master,每个Master又可以挂载多个slave
- 支持数据的高可用
- 读写分离
- 支持海量数据的读写存储操作
- 由于cluster自带Sentinel的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
- 客户端与Redis的节点连接,不再需要连接集群中所有的节点,客户端只需要任意连接集群中的一个可用节点即可
- 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系
2 Redis集群的算法——分片与槽位slot
2.1 集群中的槽位
槽位就是redis集群中用于存放key的一段空间。Redis集群中存放key的空间被分成了2^14=16384个槽位。这个槽位的序号就对应着每个key对应的CRC16算法值再取模16384的值。就是说,每个key的槽位怎么算呢?使用如下公式计算即可。
hash_slot = CRC16(key)mod 16384
CRC16 算法源码
cluster.c的KeyHashSlot方法
2.2 Redis集群的数据分片
就是每个redis主从服务,都只负责16384的一部分(一般都是平均分配),这称为分片。
2.3 使用槽位和分片 有何优势?
便于分派查询数据、集群扩容和集群缩容。
2.4 slot槽位映射算法方案对比
其实在槽位介绍时我们已经讲了本章集群使用的槽位映射算法,但实际上还有其余两种映射算法方案,我们有必要了解一下。看看到底哪一个更适合集群,更有优势。换句话说,我们为什么就使用了CRC16算法 取模 16384 外加 数据分片来映射槽位呢?
方案一 哈希取余分区
如何映射的?
计算完哈希值,直接对主机个数取余,结果为几就放在第几个主机上。
hash(key)mod 主机个数
有何劣势?
扩容缩容时,时间成本太高,要重新映射所有数据。
这种算法,在扩容或者缩容时,会将全部数据都重新洗牌一遍,比较耗时,耗费成本。
比如 原来是对3取余,现在要扩容,加一个主机,就要对4取余,那现在就要对所有数据重新洗牌,都算一算对4取余是多少,在存入对应的主机。比较耗时。同样地,缩容也有类似的缺陷和问题。
方案二 一致性哈希算法分区
如何映射的?
槽位的计算
方案一计算时,分母可能会变动,使得我们扩容缩容时花销太大,而该方案就解决了分母变动问题。它使用了一致性哈希环的设计。
一致性哈希算法必然有个hash函数并按照算法产生hash值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个hash空间[0,2^32-1],这个是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首尾相连(0 = 2^32),这样让它逻辑上形成了一个环形空间。
该环上对应着2^32个槽位,槽位计算公式。
hash(key)mod 2^32
数据分区
槽位能够定下来了,那么如何确定主机们都管理哪些槽位呢?
该算法是这样设计的。他让各个redis服务节点,映射到该环的不同位置上。
落键规则是,该key沿着顺时针方向遇到的第一个节点,就是管理该key的节点。
言外之意,从节点(redis服务)本身,逆时针走,直到遇到下一个节点停止。走过的这部分槽位,都归该节点管理。
有何优势劣势?
优势:
当需要扩容缩容时(或者某个redis服务宕机时),受影响的数据比较少。
比如,a节点宕机了,那么受影响的数据就只是a管理的这一部分数据,这部分数据会交给a的顺时针方向的下一个节点来管理。同样地,要扩容时,也只是修改一部分数据即可。
劣势:
当节点分配不合理,或者节点个数太少时,会出现节点之间管理的槽位数目不均衡的问题。
也即数据倾斜问题。
如图 这种情况下,A管理了大约80%的数据,造成节点间的不均衡。
方案三 哈希槽分区
其实就是开篇槽位讲解之处说过的映射算法,外加数据分片实现的。
hash_slot = CRC16(key)mod 16384
Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis先对key使用crc16算法算出一个结果然后用结果对16384求余数[ CRC16(key) % 16384],这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。
优势
对比方案一方案二,该方案是最优解,是生产中用的映射算法。该算法在扩容缩容时,非常友好,且不存在数据倾斜问题。因此我们直接使用的映射算法就是该方案。
扩容时,只需要把每个redis主机管理的槽位都匀出一部分给新加入的主机就可以了。缩容同理。
2.5 面试题
为什么Redis集群最大槽位要设置为16384?
redis官方设计师,安特雷兹的回答。
答:
1.正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。这意味着它们包含原始节点的插槽配置。
那么插槽配置越大,心跳数据包就会越大。
若是2^16=65535个槽位 心跳包将达到65535/8/1024=8k
若是2^14=16384个槽位 心跳包只有16384/8/1024=2k
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
2.集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。3. 槽位越小,节点少的情况下,压缩比高,容易传输Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
Redis集群能否保证强一致性?
Redis集群本身不能保证强一致性,因为它采用的是分片(sharding)的方式来水平扩展,数据被分布存储在不同的节点上。在Redis集群中,数据的写入是通过将数据分散存储在不同节点上来实现水平扩展的,每个节点只负责管理部分数据。这种设计使得Redis集群在某些情况下可能会出现数据不一致的情况,即可能存在数据写入一个节点成功,但由于某些节点未及时同步或者网络延迟等原因,导致其他节点上的数据并未更新的情况。
3 实战演示
本文采取的演示方式是,所有的redis服务都在同一虚拟机上,也就是同ip的。
3.1 三主三从集群搭建
3.1.1三主三从的配置
创建六个文件
vim /myredis/cluster/redisCluster6381.conf
vim /myredis/cluster/redisCluster6382.conf
vim /myredis/cluster/redisCluster6383.conf
vim /myredis/cluster/redisCluster6384.conf
vim /myredis/cluster/redisCluster6385.conf
vim /myredis/cluster/redisCluster6386.conf
每个文件可以按照以下内容配置
注意,port logfile pidfile dbfilename appendfilename cluster-config-file 都要设置为对应的端口号才行
bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster/cluster6381.log"
pidfile /myredis/cluster6381.pid
dir /myredis/cluster
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendonly6381.aof"
requirepass 111111
masterauth 111111
cluster-enabled yes
cluster-config-file nodes-6381.conf
cluster-node-timeout 5000
3.1.2 启动六台redis
redis-cli -a 111111 /myredis/cluster/redisCluster6381.conf
查看进程看看启动是否成功
3.1.3 配置主从关系
注意,ip地址写自己的,ip顺序无关,就按照给的命令执行也可以
redis-cli -a 111111 --cluster create --cluster-replicas 1 192.168.111.175:6381 192.168.111.175:6382 192.168.111.172:6383 192.168.111.172:6384 192.168.111.174:6385 192.168.111.174:6386
使用命令查看集群信息 ip写自己的ip 写哪一个机器随意 只要是急集群中的redis服务就行
redis-cli -a 111111 --cluster check 127.0.0.1:6381
能看到以下返回 就说明集群配置成功了
3.1.4 集群信息查看
info replication 查看主从复制的相关信息
cluster info 查看集群所有节点的信息
cluster nodes 查看集群整体信息
3.2 三主三从集群读写
新增key 发现会出现 错误 提示 请移动到某某端口
如何解决上述错误,需要开启路由,在用客户端连接服务时最后跟上-c就可以了
3.3 主从容错切换演示
关闭某个主机
查看 nodes信息发现 还是有三个主机,说明实现了从机上位的功能
可以正常使用
恢复6383的连接
6383的主机指向了6386
3.4 手动容错切换演示
3.3中我们交换了6383 和6386的身份,使得6386成为了主机,6383反而成为了从机。
那我们能不能手动的切换主从机?模拟集群自带的failover的操作?
可以的
在6383的客户端执行
cluster failover
3.5 主从扩容演示
为了扩容,我们新配置两个配置文件(内容回看本文即可找到)
开启 6387 6388的redis服务
将新增的6387作为master节点加入原有集群
redis-cli -a 111111 --cluster add-node 192.168.111.174:6387 192.168.111.175:6381
检查集群情况 6387 目前没有任何槽位
给6387分配4096个槽位
redis-cli -a 111111 --cluster reshard 127.0.0.1:6381
此时6387还没有从机,指定6388为其从机即可
需要6387主机的id 可以使用前面的集群信息查看命令 cluter nodes 来查看
redis-cli -a 111111 --cluster add-node 6388的ip:6388 6387的ip:6387 --cluster-slave --cluster-master-id 6387主机的id
redis-cli -a 111111 --cluster add-node 192.168.111.174:6388 192.168.111.174:6387 --cluster-slave --cluster-master-id 4feb6a7ee0ed2b39ff86474cf4189ab2a554a40f
检验一下 6388 确实成为了6387的从机
查看集群信息,发现扩容成功了!四主四从
3.6 主从缩容演示
加上6387 6388后,现在来实现以下缩容 删除6387 6388
要缩容,先删去从机节点
redis-cli -a 111111 --cluster del-node 127.0.0.1:6383 6383的id
查看集群信息
开始删除主节点,删除前要把6387节点的槽位移走,移到其他主节点上
redis-cli -a 111111 --cluster reshard 127.0.0.1:6381
槽位移动完毕,查看集群信息,发现6387没有槽位后,自动变味了从机,从属于6381
删去6387节点
三主三从,成功实现了缩容!!!
4 常用命令补充
带通识符的set get命令
mset k1{x} v1 k2{x} v2 ...
mget k1{x} k2{x} ...
不在同一个slot槽位下的键值无法使用mset、mget等多键操作。
可以通过{}来定义同一个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去,对照下图类似k1k2k3都映射为x,自然槽位一样。
这也就解释了为什么CRC16 算法源码 cluster.c的KeyHashSlot方法 中有对 “{” 和 “}” 的判断
集群是否完整才能对外提供服务的配置
默认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 ,这样的话你挂了的那个小集群是不行了,但是其他的小集群仍然可以对外提供服务。
CLUSTER COUNTKEYSINSLOT 槽位数字编号
例如 执行CLUSTER COUNTKEYSINSLOT 1234
若返回:
1,该槽位被占用
0,该槽位没占用
CLUSTER KEYSLOT 键名称
例如 cluster keyslot k1 就会返回该key的槽位