即使是使用哨兵,此时的Redis集群的每个数据库依然存有集群中的所有数据,从而导致集群的总数据存储量受限于可用存储内存最小的节点,形成了木桶效应。而因为Redis是基于内存存储的,所以这一个问题在redis中就显得尤为突出了
在redis3.0之前,我们是通过在客户端去做的分片,通过hash环的方式对key进行分片存储。分片虽然能够解决各个节点的存储压力,但是导致维护成本高、增加、移除节点比较繁琐。因此在redis3.0以后的版本最大的一个好处就是支持集群功能,集群的特点在于拥有和单机实例一样的性能,同时在网络分区以后能够提供一定的可访问性以及对主数据库故障恢复的支持。
哨兵和集群是两个独立的功能,当不需要对数据进行分片使用哨兵就够了,如果要进行水平扩容,集群是一个比较好的方式
一、redis的集群安装和演示
redis的集群安装可以在网上自己查找一个,可以参考这篇博客https://blog.csdn.net/qq_32364027/article/details/79568701
大概的思想就像修改redis的配置文件:
daemonize yes #后台启动
port 7001 #修改端口号,从7001到7006
cluster-enabled yes #开启cluster,去掉注释
cluster-config-file nodes.conf
cluster-node-timeout 15000
appendonly yes
然后复制配置文件,然后分别制定配置文件启动,然后按照ruby环境
redis5的版本可以使用redis-cli --cluster这个命令,本文章使用的版本是redis5的版本
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
如果想要简单的实验,可以参考redis第5版本的脚本工具方法,可以参考官方文档关于redis的集群安装: https://redis.io/topics/cluster-tutorial
在redis解压缩之后utils/create-cluste目录下有个create-cluster 脚本
可以通过create-cluster启动6台redis服务,创建集群,还有关闭集群
命令如下 :
sh create-cluste start //启动6台redis服务
sh create-cluste create //创建集群
sh create-cluste stop //关闭redis服务
- 启动完之后,查看启动redis的进程情况
ps -ef | grep redis
输出信息如下:
root 15113 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30001 [cluster]
root 15118 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30002 [cluster]
root 15123 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30003 [cluster]
root 15128 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30004 [cluster]
root 15133 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30005 [cluster]
root 15138 1 0 22:41 ? 00:00:01 ../../src/redis-server *:30006 [cluster]
- 查看集群节点配置
src/redis-cli --cluster check 127.0.0.1:30004
查看节点端口号为30004节点的情况,如下信息可以知道30001和30002和30003是主节点,其他的为三个从节点,并且初始化的0-5460的槽位分配给了30001端口这台redis服务,10923-16383的槽位分配给了30002端口这台redis服务,5461-10922的槽位分配给了3003这台redis服务
输出信息如下:
127.0.0.1:30001 (286b941b...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:30003 (c13670ac...) -> 0 keys | 5461 slots | 1 slaves.
127.0.0.1:30002 (5aa94b77...) -> 0 keys | 5462 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 127.0.0.1:30004)
S: 09c5d3e9a01421cac00557c6ac5e7b264826aa8c 127.0.0.1:30004
slots: (0 slots) slave
replicates c13670ac6ce8b9a3b4ab9fe602830f05ad9b1c65
M: 286b941bd1cc696f4bbde526b51b2549a873727c 127.0.0.1:30001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: c13670ac6ce8b9a3b4ab9fe602830f05ad9b1c65 127.0.0.1:30003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 5aa94b777af9c38340335ffd4347017c312bb90a 127.0.0.1:30002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: d613afabc52649efb022706ee83d8b98296559ad 127.0.0.1:30005
slots: (0 slots) slave
replicates 286b941bd1cc696f4bbde526b51b2549a873727c
S: 2a249301a86d2796a24f9cbe86d3699c82b9a0dd 127.0.0.1:30006
slots: (0 slots) slave
replicates 5aa94b777af9c38340335ffd4347017c312bb90a
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
- 通过客户端连接redis服务
src/redis-cli -p 30001
此时给key设置,会发送move动作,原因是key会通过公式:slot = CRC16(key)%16383算出key的槽位,此时的计算出槽位是12539是位于30003这台redis服务的槽位的,所以设置值应该由30003这台服务设置
- 给一台主节点设置值
127.0.0.1:30001> set key 123
(error) MOVED 12539 127.0.0.1:30003
- 读取一台主节点的值
在原有的30003的节点设置键为key,值为123成功之后,切换到30001这台节点之后,获取设置键的值,同理可以知道30001的值并不存在,所以获取不到,只能在30003这个节点获取
[root@localhost redis-5.0.3]#src/redis-cli -p 30001
127.0.0.1:30001> get key
(error) MOVED 12539 127.0.0.1:30003
- 读取副本的值
此时3004已经存有key的值,数据已经从30003节点master同步过来了,但是get key的时候还是发生了moved 命令
[root@localhost redis-5.0.3]# src/redis-cli -p 30004
127.0.0.1:30004> keys *
1) "key"
127.0.0.1:30004> get key
(error) MOVED 12539 127.0.0.1:30003
二、redis的拓扑结构
一个Redis Cluster由多个Redis节点构成。不同节点组服务的数据没有交集,也就是每个一节点组对应数据
sharding的一个分片。节点组内部分为主备两类节点,对应master和slave节点。两者数据准实时一致,通过异步
化的主备复制机制来保证。一个节点组有且只有一个master节点,同时可以有0到多个slave节点,在这个节点组中
只有master节点对用户提供些服务,读服务可以由master或者slave提供
redis-cluster是基于gossip协议实现的无中心化节点的集群
,因为去中心化的架构不存在统一的配置中心,各个节点对整个集群状态的认知来自于节点之间的信息交互。在Redis Cluster,这个信息交互是通过Redis Cluster Bus
来完成的
三、Redis的数据分区
分布式数据库首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集, Redis Cluster采用哈希分区规则,采用虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。比如Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。
计算公式:slot = CRC16(key)%16383。每一个节点负责维护一部分槽以及槽所映射的键值数据
四、HashTags
通过分片手段,可以将数据合理的划分到不同的节点上,这本来是一件好事。但是有的时候,我们希望对相关联的业务以原子方式进行操作。举个简单的例子
我们在单节点上执行MSET , 它是一个原子性的操作,所有给定的key会在同一时间内被设置,不可能出现某些指定的key被更新另一些指定的key没有改变的情况。但是在集群环境下,我们仍然可以执行MSET命令,但它的操作不在是原子操作,会存在某些指定的key被更新,而另外一些指定的key没有改变,原因是多个key可能会被分配到不同的机器上
所以,这里就会存在一个矛盾点,及要求key尽可能的分散在不同机器,又要求某些相关联的key分配到相同机器
从前面的分析中我们了解到,分片其实就是一个hash的过程,对key做hash取模然后划分到不同的机器上。所以为了解决这个问题,我们需要考虑如何让相关联的key得到的hash值都相同呢?如果key全部相同是不现实的,所以怎么解决呢?在redis中引入了HashTag的概念,可以使得数据分布算法可以根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片
举个简单的例子,加入对于用户的信息进行存储, user:user1:id、user:user1:name那么通过hashtag的方式,user:{user1}:id、user:{user1}:name; 表示当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash。
如下图所示,可以发现user:{user1}.id经过CRC16对key进行计算取模之后和对user1经过CRC16对key进行计算取模之后,跳转的槽位都为8106,说明对user:{user1}.id的键只对user1进行计算。
[root@localhost redis-5.0.3]# src/redis-cli -p 30004
127.0.0.1:30004> set user:{user1}.id 123
(error) MOVED 8106 127.0.0.1:30002
127.0.0.1:30004> set user1 123
(error) MOVED 8106 127.0.0.1:30002
五、重定向客户端
Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,这个节点是怎么处理的呢?比如我想获取key为msg的值,msg计算出来的槽编号为12539,当前节点正好不负责编号为12539的槽,那么就会返回客户
端下面信息:
127.0.0.1:30004> get key
(error) MOVED 12539 127.0.0.1:30003
表示客户端想要的12539槽由运行在IP为127.0.0.1,端口为30003的Master实例服务。如果根据key计算得出的槽恰好由当前节点负责,则当期节点会立即返回结果
六、分片迁移
在一个稳定的Redis cluster下,每一个slot对应的节点是确定的,但是在某些情况下,节点和分片对应的关系会发生变更
- 新加入master节点
- 某个节点宕机
也就是说当动态添加或减少node节点时,需要将16384个槽做个再分配,槽中的键值也要迁移。当然,这一过程,在目前实现中,还处于半自动状态,需要人工介入。
- 新增一个主节点
新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。大致就会变成这样:
节点A覆盖1365-5460
节点B覆盖6827-10922
节点C覆盖12288-16383
节点D覆盖0-1364,5461-6826,10923-12287
这一章节具体的可以看博客https://blog.csdn.net/qq_32364027/article/details/79568701的第五小节。写的比较清楚
(1)先启动一台redis服务,配置可以参考第一节
src/redis-server redis.conf
(2) 查看一下redis的服务情况
[root@localhost redis-5.0.3]# ps -ef | grep redis
root 15113 1 0 2018 ? 00:00:20 ../../src/redis-server *:30001 [cluster]
root 15118 1 0 2018 ? 00:00:21 ../../src/redis-server *:30002 [cluster]
root 15123 1 0 2018 ? 00:00:21 ../../src/redis-server *:30003 [cluster]
root 15128 1 0 2018 ? 00:00:21 ../../src/redis-server *:30004 [cluster]
root 15133 1 0 2018 ? 00:00:21 ../../src/redis-server *:30005 [cluster]
root 15138 1 0 2018 ? 00:00:21 ../../src/redis-server *:30006 [cluster]
root 15568 1 0 00:14 ? 00:00:00 src/redis-server *:40000 [cluster]
(3)让新启动的节点加入集群
add-node是加入集群节点,127.0.0.1:40000为要加入的节点,127.0.0.1:700230001表示加入的集群的一个节点,用来辨识是哪个集群,理论上那个集群的节点都可以。
src/redis-cli --cluster add-node 127.0.0.1:40000 127.0.0.1:30001
(4)查看集群的情况
src/redis-cli -c -p 30001
127.0.0.1:30001> cluster nodes
(5)重新分配卡槽
redis-cluster在新增节点时并未分配卡槽,需要我们手动对集群进行重新分片迁移数据,需要重新分片命令
src/redis-cli --cluster reshard 127.0.0.1:40000
这个命令是用来迁移slot节点的,后面的127.0.0.1:40000是表示是哪个集群,端口填[30001-30006,40000]都可以
- 类比增加从节点用
src/redis-cli --cluster add-node --slave 127.0.0.1:40001 127.0.0.1:30001
- 删除一个主节点
先将节点的数据移动到其他节点上,然后才能执行删除
具体还是这一章节具体的可以看博客https://blog.csdn.net/qq_32364027/article/details/79568701的第六小节
七、槽迁移的过程
槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得RedisCluster不必宕机的情况下可以执行槽的迁移。下面这张图描述了我们迁移编号为1、2、3的槽的过程中,他们在MasterA节点和MasterB节点中的状态。
简单的工作流程
- 向MasterB发送状态变更命令,把Master B对应的slot状态设置为
IMPORTING
- 向MasterA发送状态变更命令,将Master对应的slot状态设置为
MIGRATING
当MasterA的状态设置为MIGRANTING后,表示对应的slot正在迁移,为了保证slot数据的一致性,MasterA此时对于slot内部数据提供读写服务的行为和通常状态下是有区别的,
MIGRATING状态
(1) 如果客户端访问的Key还没有迁移出去,则正常处理这个key
(2) 如果key已经迁移或者根本就不存在这个key,则回复客户端ASK信息让它跳转到MasterB去执行
IMPORTING状态
当MasterB的状态设置为IMPORTING后,表示对应的slot正在向MasterB迁入,及时Master仍然能对外提供该slot的读写服务,但和通常状态下也是有区别的
(1) 当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的并且还存在于MasterA上的key,如果此时这个key在A上已经被修改了,那么B和A的修改则会发生冲突。所以对于MasterB上的slot上的所有非ASK跳转过来的操作,MasterB都不会处理,而是通过MOVED命令让客户端跳转到MasterA上去执行
这样的状态控制保证了同一个key在迁移之前总是在源节点上执行,迁移后总是在目标节点上执行,防止出现两边同时写导致的冲突问题
。而且迁移过程中新增的key一定会在目标节点上执行
,源节点也不会新增key,使得整个迁移过程既能对外正常提供服务,又能在一定的时间点完成slot的迁移。