一)什么是Redis集群
1.1)集群这个词,只要是多个机器,构成了分布式系统,都是可以称之为是一个集群,前面学的主从复制和哨兵模式,也可以认为是广义的集群,狭义的集群就是redis提供的集群模式,这种集群模式下主要是为了解决存储空间不足的问题,来拓展存储空间
1.2)哨兵模式本质上提高了系统的可用性,哨兵模式中本质上还是主从节点来存储数据,这样就会要求主节点或者是从节点,就得存储整个数据的全集数据,内存只有256G,512G
此处的关键问题就是引入多台机器,每一台机器来存储一部分数据,假设现在有1TB的数据需要存储,但是在这里不是说光高三个机器就够了,而是每一个存储数据的机器还需要搭配若干个从节点
1.3)引入多个主节点,多个主节点存储不同部分的数据,从而能够使整个redis集群存储更多的数据,同时为了保证可靠性,保证数据的可用性,还要给每一份保存数据的主节点引入多个从节点,来去作进一步的备份和容灾
二)哈希求余算法
1)借助哈希表的思想,就是借助一个哈希函数,把一个key映射到一个整数,在来针对于数组的长度进行求余,就可以映射到数组下标上
2)比如说现在有三个分片,编号为0 1 2,此时就可以针对这里面的数据的key,redis都是键值对的数据,计算hash值,比如说使用md5,再把这个hash值取余上分片个数,就得到了一个下标,就可以放到该下标对应的分片中,后续再来查询key的时候,也是同样的算法,key是相同的,hash函数相同,得到的分片是相同的
3)md5本身就是一个计算hash值的算法,针对一个字符串,对里面的内容进行一系列的数学变换,是一个16进制的数字,MD5是一个非常广泛使用的算法,特点:
4)一旦服务器集群扩容就需要更高的成本,分片主要的目的就是为了能够提升存储能力,分片越多,存储的数据越多,成本也就越高,一般都是先少搞几个分片,但是随着业务逐渐增长,数据变多了,三个分片就不容易进行保存了,此时就需要扩容,当hash函数和key都不变的情况下,如果N发生改变了,那么整体的分片结果仍然会变,如果发现某一个数据,再进行扩容以后,不应该呆在原来的分片中,就需要进行重新分配,搬运数据,不光是原来的主节点需要删除数据,新的主节点还要增加数据,况且还涉及到从节点,此时开销非常大,CPU资源,网络带宽资源会吃满,不能在线上环境进行操作,只能通过替换的方式进行操作,扩容搬运成本大;
三)一致性哈希算法:
1)将0-2^32-1这个数据空间,映射到一个圆环上,数据按照顺时针的方向进行增长
2)假设现在存在三个分片,就把这个分片放到圆环上的某一个位置
3)假定有一个key,计算得到的哈希值是H,那么这个key映射到那个分片呢,规则很简单就是从H所在位置,顺时针往下找,找到的第一个分片,就是属于key所属的分片
这就相当于是N个分片的位置,把整个圆环分成了N个管辖区间,Key的hash值落在某一个区间内,就归对应的区间进行管理
在hash求余这种操作中,当前key是属于哪一个分片,是交替的,在一致性哈希这样的设定下,把交替出现变成了连续分片
1)现在就只是需要将0号分片上面的这一段数据搬运到三号分片上面即可,2,3号分片都是不变的,刚才这种搬运的成本,也是有的,但是比刚才哈希求余的方式低了不少,虽然搬运的成本低了,但是这几个分片的数据量就不均匀了,也就是数据倾斜,3号 0号数据比较少,其他节点的数据比较多,1 2机器压力比较大;
2)如果一次扩容高多个分片,在2号到1号分片加上一个机器,等等,的确是一个好的思路,可以避免数据倾斜的倾斜,总的搬运数据量仍然比哈希求余的方式更少;
3)但是领导不一定批这么多机器,进行扩容不一定是配套一个机器就行了,还有一些配套的从机,所以可能就是一批机器,运维成本太高了,管理成本很大,内存占用少点,成本太高了,所以一致性哈希算法,但是引入了数据倾斜问题,既能解决数据倾斜,又能解决一致性哈希算法
四)哈希槽分区算法:
1)hash_slot=crc(16)%16384,16384=16*1024=16K,会把上述的这些哈希槽,分配到不同的分片上,假设现在有三个分片,把这些哈希槽平均分成三份,虽然不是严格的意义上的均匀但是差距非常小,这种算法本质上就是把一致性哈希和哈希求余结合一下
2)当然这种只是一种可能的分片方式,实际上分片是非常灵活的,每一个分片持有的槽位号,可以是连续的,也可以是不连续的,此处每一个分片都会使用位图这样的数据结构表示当前有多少个槽位号,16384个bit,用每一位0/1来区分当前这个分片是否持有该槽位号
针对于某一个分片,上面的槽位号,不一定非得是连续的区间
3)在上述过程中,只有被移动的槽位,对应的数据才需要被搬运,在redis中某一个分片包含哪些槽位都是可以灵活的手动配置的;
一)redis集群本身最多只是有16384个分片吗?
1.1)每一个分片只有一个槽位,key是需要先映射到槽位,再来映射到分片上的,如果每一个分片包含的槽位比较多,如果说槽位个数相当,就可以认为是key数量相当,但是如果每一个分片包含的槽位非常少,槽位个数不一定可以直观的反应到key的个数,可能有的槽位有多个key,有的槽位没有key
1.2)现在如果每一个分片只有一个槽位,很难保证数据key在各个分片上面的均衡性,实际上redis作者建议分片数目不超过1000,如果真的是1.6W个分片,那么整个数据服务器集群规模就太可拍了,因为肯定是不光1.6W台服务器,还有从服务器呢
二)为什么要有16384个槽位?
因为不同的分片之间要使用心跳包来进行通信,心跳包里面就包含了该节点持有哪些槽位,虽然8KB也比2KB大不了多少,但是心跳包是周期性进行通信的,非常频繁非常吃网络带宽,这个值基本够用了,同时占用的网络带宽又不是很大
集群的定义:所谓的集群,就是通过增加服务器的数量,提供相同的服务,从而让服务器达到一个稳定、高效的状态
1)之前的哨兵模式是存在着一些问题的,因为如果主节点挂了,那么sentinel集群会选举新的sentinel兵王来去进行故障修复操作,一旦此时受到了客户端的写的请求,就有可能会造成数据丢失的情况
2)主从集群可以应对高并发读的问题,redis主从之间要做同步,单节点的内存设置不要太高,如果内存占用的过高,那么在做RDB的持久化或者全量同步的时候就会导致大量的IO,性能下降
2.1)如果写的并发也很多怎么办呢?
2.2)如果需要进行存储的数据是海量数据,存储的数据量仍然是很多怎么办呢?
主从模式和哨兵模式可以解决高并发读,高可用的问题,但是仍然存在着海量数据存储的问题和高并发写的问题
2.3)分片集群的特征:
2.3.1)集群中有多个master,每一个master保存着不同的数据,这样进行存储的数据总量取决于master结点的数量,并发写,并发读的性能都提升了;
2.3.2)每一个master本身都有多个slave节点
2.3.3)master相互之间可以做ping命令来进行检测检测彼此的健康状态
1)Redis集群支持多个Master,每一个Master又可以挂载个Slave,这样可以实现读写分离
支持数据的高可用,支持海量数据的读写存储操作;
2)由于Cluster自带的Sentinel的故障转移机制,内置了高可用的支持,这是无需再次使用哨兵模式;
3)客户端和Redis的节点相连,不再需要连接集群中的所有节点,只需要进行任意连接集群中的一个可用节点即可
4)槽位slot负责分配到各个物理服务节点,有对应的集群来负责维护节点,插槽和数据之间的关系
五)搭建redis集群:
1)在/myredis目录下面创建9001 9002 9003 9004 9005 9006 900目录,并分别写入redis.conf文件
2)在redis.conf文件中配置下面的信息
3)启动各个目录下面的redis,虽然此时6个redis实例,全部启动起来了,但是它们彼此之间并没有相互联系起来,因为并没有进行指定谁和谁之间是有联系的
port 9001 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称,不需要我们创建,只需要程序员指定位置即可这个文件将来由redis自己维护 cluster-config-file /myredis/9001/nodes.conf # 节点心跳失败的超时时间,如果集群之间相互做心跳的时候超过5s没有心跳,就认为是疑似宕机了 cluster-node-timeout 5000 # 持久化文件存放目录 dir /myredis/9001 # 绑定地址,任何地址都可以访问 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例ip # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /myredis/9001.log
4)创建redis集群:
redis-cli --cluster create 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 127.0.0.1:9004 127.0.0.1:9005 127.0.0.1:9006
--cluster-replicas 2
• --cluster create 表⽰建⽴集群,后⾯填写每个节点的ip地址和端口号
• --cluster-replicas 1表⽰每个主节点需要1个从节点备份-c选项添加之后,redis客户端就会根据当前key时机计算出来的槽位号,自动的找到匹配的分片主机,进一步就可以完成该操作,使用集群以后,之前学过的命令,都能正常使用,不够严谨,有些指令,是能够操作多个key的,如果此时key分散在不同的分片上,就可能出现问题了
1.1)这里面的redis-cli --cluster代表的是集群操作命令
1.2)create代表的是创建集群
1.3)--cluster-replicas 1,表示给每一个master节点分配一个从节点,代表指定集群中每一个master得副本是1,此时节点总数/(replicas+1)得到的就是master得数量,因此上面的节点列表的前n个就是master,其他节点的都是slave节点,被随机分配到不同的master节点
3)在进行连接集群中的某一个节点的时候,直接输入命令
redis -c -p 具体的端口号
4)查看某一个key属于哪一个对应的槽位值,使用命令cluster keysolt key的名称
1)查看集群状态:redis-cli -p 9001 cluster nodes
2)Redis会把每一个master节点映射到0-16383一共是16384个插槽上面,查看集群信息的时候就可以看到,所有的插槽会给每一个节点分配一部分
3)Redis中的key不是和master节点进行绑定的,而是和插槽进行绑定的,redis会根据key的有效部分来进行计算插槽的值,主要是分成两种情况:
3.1)key中包含{},况且大括号内部至少包含一个字符,那么大括号中的部分就是有效部分
3.2)key中不包含大括号,整个key都是有效部分
假设如果key是num,那么key的有效部分就是num,但是假设key是{kkk}num,那么有效部分就是kkk,计算方法就是利用CRC16算法得到一个哈希值,针对这个16384取余,最后得到的结果一定是在0-16373之间,然后得到一个solt值;
4)为什么key要和插槽绑定而不是节点绑定呢?因为redis的主节点是有可能出现宕机的情况的,或者是集群扩容增加了节点,或者是集群伸缩,删除了节点,如果一个节点宕机了,那么这个节点上面的数据也就丢失了,而数据如果是和插槽绑定的,那么当节点宕机的时候,可以把这个节点对应的插槽转移到正常工作的节点,集群扩容的时候,也可以将插槽进行转移,这样数据跟着插槽走,就永远可以找到数据的位置;
5)redis-cli -p 9001 -c来连接对应的集群中的节点,当进行查询或者是插入key的时候,redis会先进行计算插槽值,在来进行判断这个key所在的插槽应该在哪一个节点上,完成一个请求的路由和重定向;
所以当你进行操作任意一个key的时候,会先进行计算插槽值,再进行判断你在哪一个节点,在完成一个请求的路由,所以你进行访问任意一个数据,都可以重定向到具体的节点;
cluster nodes查看集群结构
六)集群伸缩:
redis-cli --cluster add-node 127.0.0.1:9001 --cluster-slave c755f48475bbc8b5b52e9f2051ac1ea54d4d4ff8
redis-cli --cluster提供了很多可以进行操作集群的命令,可以通过以下方式来进行查看:
1)如果想要进行新增节点,需要指明新增加节点的IP地址和新的端口号,还要进行指明集群中已经存在的IP和端口号(为了新增节点的时候通知每一个集群中每一个角色,从而联系上这个集群)
2)下面的--cluster-slave和--cluster--master-ip默认是没有进行添加的,如果没有添加这两个节点,那么这个新增的节点默认就是一个主节点,如果你添加了这两个信息,那么默认这个新增的节点就成为了指定节点的从节点了
3)需求:向集群中添加一个新的master节点,并向其中存储num=10
3.1)启动一个新的redis实例,端口号是1000
3.2)添加1000到之前的集群,并作为一个master节点
3.3)给7004节点分配插槽,使得num这个key可以存储到7004这个主节点上面
1.1)添加1000新节点到集群中
执行完成这个命令这个1000节点就成功地被加入到集群里面了
redis-cli --cluster add-node 127.0.0.1:1000 127.0.0.1:9001
1.2)通过命令查看集群状态
redis-cli -p 1000 cluster nodes
1.3)查看集群状态可知
1000的插槽数量是0,因此没有任何节点可以成功存储到1000这个主节点
1.4)先进行查看num的插槽值是多少,可以看到num的插槽值是2765,也就是说只要2765这个插槽能够成功分配到1000节点,那么1000这个主节点就可以成功的存储到num值了
redis-cli -p 9001 -c
1.5)先查看以下分配插槽的命令:不然新增的节点就没有任何意义了
1.6)建立连接: redis-cli --cluster reshard 127.0.0.1:9001
reshard后面指定的集群中的任意一个节点的IP地址和端口号
1.7)当你输入命令成功之后,redis会想你进行询问你想要那一部分的插槽,我们需要转移3000个插槽就足够了,就是把节点9001上面的前3000个插槽移动过去即可
1.8)接下来redis有会向你进行询问,你需要将插槽转移到哪里去?所以我们需要直接输入1000插槽对应的ID即可
1.9)然后redis还是会向你进行询问,你这些插槽,从哪一个主节点开始进行拷贝,请输入主节点的ID,当然是从9001开始呀,于是输入9001对应的ID即可,然后输入done即可
集群缩容
1)先删除主节点上面所对应的从节点:
redis-cli --cluster del-node 从机IP:从机端口 从机的ID
redis-cli --cluster del-node 127.0.0.1:9005 523b1cb07bec09c6c98a2218cbe247fa0b56c757
2)将9001对应的槽清空,将对应的槽位全部分配给1000
3)删除9001节点: redis-cli --cluster del-node
redis-cli --cluster del-node 127.0.0.1:9001 50538e5db5878cf0acf0a0d0b1b57331c71110d2
redis-cli --cluster add-node 127.0.0.1:9001 127.0.0.1:9003 --cluster-slave c755f48475bbc8b5b52e9f2051ac1ea54d4d4ff8
整个集群都是一个整体,连接上一个节点都在操作整个集群
进行测试Sentinel池:
@Controller public class RestController { @RequestMapping("/Java100") @ResponseBody public String start(){ //1.配置信息 HashSet
set=new HashSet<>(); // 连接信息 ip:port // set.add("127.0.0.1:27001"); set.add("124.71.136.248:27001"); //2.创建Sentinel连接池 JedisSentinelPool pool = new JedisSentinelPool("mymaster", set, "12503487"); //3.获取到Jedis客户端 Jedis jedis=pool.getResource(); //4.设置元素 jedis.set("name","zhangsan"); String value=jedis.get("name"); return value; } } public static void main(String[] args) { RedisURI redisURI = RedisURI.builder() .withHost("124.71.136.248") .withPort(6379) .withPassword("12503487") .build(); RedisClient redisClient = RedisClient.create(redisURI); // // 创建 RedisClient // RedisClient redisClient = RedisClient.create("redis://124.71.136.248:7001"); // 获取与 Redis 单节点的连接 StatefulRedisConnection
connection = redisClient.connect(); // 获取同步命令 RedisCommands syncCommands = connection.sync(); // 执行 Redis 命令 syncCommands.set("key", "value"); String value = syncCommands.get("key"); System.out.println("Value: " + value); // 关闭连接 connection.close(); redisClient.shutdown(); }
1)不一致哈希取余分区:2亿条记录就是2亿个K,V,我们单机是存储不下这些数据的,必须要使用分布式机器,假设有三台机器构成一个集群,用户每一次的读写操作都是根据公式:
hash(key)%机器数,来进行计算出哈希值,用来决定数据映射到哪一个机器上面
1.1)优点:简单粗暴,直接有效,只需要预估好数据,规划好节点,使用哈希算法让固定的一部分请求落在同一台服务器上面,这样每一台服务器来固定处理一部分请求,并且维护这些请求的信息,起到负载均衡和分而治之的作用;
1.2)缺点:原来规划好的节点,进行扩容和缩招就比较麻烦,因为映射关系要重新进行计算,除非服务器永远不会变化,在服务器固定个数不变的时候没有问题,但是如果需要进行弹性扩容或者故障停机的情况下,原来的取模公式就会发生变化,Hash(Key)/3就会变成Hash(Key)/2,甚至于说如果某一台Redis直接宕机了,由于台数发生变化,就会导致哈希取余全部数据重新洗牌;
二)一致性哈希算法分区:设计目标是为了解决分布式缓存数据变动和映射问题,如果某一台机器宕机了,分母数量改变了,自然取余数就不OK了,就是为了当服务器的个数发生变动的时候,要尽量减少客户端到服务器的映射关系
2.1)哈希取余分区:一致性哈希是将整个哈希值空间组织成一个虚拟的圆环,比如说假设某一个哈希值的值空间是0-2^32-1,那么他的整个哈希环空间如下,也就是说所有的输入值都被映射到0-2^32之间,组成了一个圆环,前面介绍的算法是针对服务器的数量进行取余,而一致性哈希算法是针对2^32进行取余;
2.2)下一步将各个服务器使用Hash函数进行一个哈希, 具体可以选择服务器的ip或主机名或者其他业务属性作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用ip地址哈希后在环空间的位置如下:
2.3)将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并且将该键值对存储到该节点上面
3)哈希槽算法
1)因为哈希槽本身就是一个数组,数组[0,2^14-1]会形成hashslot空间
2)为了解决均匀分配的问题,在数据和节点之间新加了一层,把这一层称之为是哈希槽,用于管理数据和节点之间的关系,现在就相当于是节点上存放的是槽,槽里面存放的是数据
3)一个集群只能有16384个槽,编号为0-16343,这些槽会分配给集群中的所有主节点,分配策略是没有要求的,在集群中会进行记录节点和槽的对应关系,解决了节点和槽的映射关系之后,余数是几,key就落到对应的槽里面
在启动的时候redis-cli的时候,加上-c选项,此时客户端发现当前key的操作不在当前分片上,就能够自动的重定向到对应的主节点上
七)自动故障转移:
1)如果在集群中有节点挂了,如果挂了的是从节点,没事,注意主节点才可以执行写操作,从节点不行,当尝试在从节点上进行写操作,此时就会被自动重定向到指定的主节点上
2)如果主节点挂了,那么此时集群所做的工作就是和之前的哨兵类似了,就会自动地把该主节点下面的从节点,挑选出来一个,提拔成主节点
3)集群的机制也能处理故障转移,此处的故障转移,具体处理流程和哨兵处理流程不太一样
八)手动故障转移:
1)先实现将节点断开连接redis -p 对应节点的端口号 shutdown
2)将这个节点重启,发现这个节点已经变成了从节点
当一个集群中有一个master宕机会发生什么呢?
1)首先是该实例会和其他实例断开连接
2)然后是疑似宕机
3)最后是确认下线,自动提升一个该节点的slave节点为新的master节点
从上面的日志信息我们可以看出,当我们把9001这个进程给干掉之后,9005就成为了主节点,如果我们再次启动9001,那么它会变成分片集群中的从节点
手动故障转移:
1)在从节点利用cluster master命令可以让集群中的这个从节点对应的master节点完成数据转移-切换到cluster master命令的这个slave节点,实现无感知的数据迁移
2)具体的替换流程如下:现在我想让这个slave节点替换master节点再让原来的主节点宕机停掉
2.1)当去执行命令的这一刻,slave节点会向master节点发送一个消息,和master说我要替换你了,这个时候主节点为了避免消息的丢失,这个master节点就会拒绝客户端的一切请求,所有进来的命令请求的这个master节点都会进行阻塞;
2.2)master会返回一个offset给slave
2.3)slave也会等待当前的数据和master一致,如果不一致赶紧做主从同步,等待主节点和从节点数据一致
2.4)开始做故障迁移
2.5)让slave充当master,master来充当slave
2.6)这个新的master结点开始做一个广播,通知自己是新的节点
3)这种Failover支持三种不同版本的参数
3.1)不加任何参数,按照默认的流程1-6来进行节点替换
3.2)fource,省略了对offset的一致性校验,不管数据是否相同
3.3)takeover:直接执行2.6,忽略数据一致性,忽略master状态和其他master的意见,属于最暴力的做法