目录
单点Redis的问题
1.数据丢失问题 :Redis是内存存储 。重启服务可能会丢失数据
2.并发能力 :高并发场景下
3.故障恢复问题 :Redis宕机,服务就不可用
4.存储能力:单点存储的数据有限
基于Redis集群解决单机Redis存在的问题
1.Redis数据持久化
2.redis主从集群 读写分离 s
3. Redis哨兵,健康检测和自动恢复
4.分片集群,动态孔融
Redis数据持久化
两种方案实现数据持久化:RDB和AOF
RDB持久化
RDB文件称为快照文件。
实现方式
把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件来恢复数据。
命令
save :由Redis主进程来执行。Redis是单线程的因此是阻塞命令。
bgsave:开启一个子线程执行RDB,可以异步执行RDB,不影响Redis主进程执行,不会阻塞其他命令执行。
如何触发RDB
默认情况下,Redis正常停机(关机)时会执行一次RDB。但是突然宕机是来不及执行RDB。
使用bgsave语法:save <秒数> <key被修改次数>
RDB原理——fork(复刻)
bgsave开始时会fork(复刻)主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,子进程访问共享内存;
- 当主进程执行写操作时,则会拷贝一份数据,内存中多一份数据,主进程操作备份数据执行写操作。
注意
子进程共享主进程的内存数据指的是:主进程创建子进程,然后子进程复刻主进程中的页表。采用页表的映射方式就只用复制映射物理内存的页表即可,不需要复制物理内存中的实际内存数据,非常的快捷和节省内存空间。
AOF持久化
AOF全称为Append Only File(追加文件)。存储的是Redis的命令。
AOF把每条Redis执行的命令都记录下来,读取AOF文件的时候从一个空的Redis开始执行记录的命令。
记录命令频率的三种策略及性能对比
appendfsync always:每执行一次写命令,立即记录到AOF文件。
appendfsync always:写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案。不需要主进程自己去执行写入磁盘操作,异步操作。
appendfsync no:写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。不需要主进程自己去执行写入磁盘操作,异步操作。
性能对比:
AOF文件重写命令(BGREWRITEAOF)
执行bgrewriteaof命令,让AOF文件执行重写功能。通过算法把AOF中记录的无效命令去除,最大化整合有效命令,同时做到最终结果一致,节省AOF文件占用空间。
如何触发AOF
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置
# AOF文件比上次文件 增长超过多少百分比则触发重写,默认为 100
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写,默认为 64mb
auto-aof-rewrite-min-size 64mb
持久化方案两者之间区别
1、RDB是完全的异步操作,AOF除了频率为appendfsync always情况外,也都是异步操作。
2、AOF和RDB同时存在的混合模式也是可以的,如果AOF和RDB同时存在的时候,RDB和AOF的写入互不干扰,但是读取的话,Redis会优先使用从AOF文件来还原数据库状态,如果AOF关闭状态时,则从RDB中恢复。
3、在Redis版本更新的计划中,计划把RDB和AOF两者融合为一种,因为RDB和AOF混合使用非常常见。
4、AOF的重写命令BGREWRITEAOF会占用大量CPU和内存资源。
Redis主从集群
读写分离策略
采用读写分离,而非负载均衡。对Redis的操作大部分都是读,写操作占少数,所以在集群中采用读写分离。
实现主从数据同步
全量同步
第一次主从同步是全量同步。
注意事项
1.master如何判断slave是不是第一次来同步数据?
Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave会继承master节点的replid。
offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据。
2.全量同步的流程:
1、slave节点请求增量同步
2、master节点判断replid,发现不一致,拒绝增量同步,向slave返回master的replid和offset让其记住
3、master将完整内存数据生成RDB,发送RDB到slave
4、slave清空本地数据,加载master的RDB
5、master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
6、slave执行接收到的命令,保持与master之间的同步
增量同步
repl_backlog原理:
全量同步时的repl_baklog文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset。master怎么知道slave与自己的数据差异在哪里呢?
slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。
随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset。
实现数据同步的完整流程
1、slave第一次向master发送请求,发送的是增量同步请求,此时的replid和offset就是slave自身的,因为slave成为slave节点之前,自身就是一个master,即使它没有slave节点,所以replid和offset是它自身的。
2、master收到slave的请求后,会先判断replid是否一致,如果是第一次,master会拒绝slave的增量请求,然后返回master的replid和offset给slave,让slave记住。同时master会使用bgsave开启新线程,向这个第一次来的slave进行RDB全量同步。
3、由于RDB全量同步非常耗时,那么在这期间master数据会改变,期间的命令全都会被master记录在repl_baklog中,并修改自己的offset,然后等待RDB全量同步完成后,通过比较replid和offset,来进行增量同步(类似于AOF),然后发送repl_caklog的命令给slave让其执行。
4、那么要是在发送repl_caklog期间,master又改变了,结果是依旧会被记录到repl_caklog中,然后比较replid和offset,再次然后发送repl_caklog的命令给slave让其执行。这是一个类似于监听的任务,只要不是第一次以后,都是借助replid和offset、repl_caklog来实现的增量同步。
优化Redis主从集群
1、在master中配置repl-diskless-sync yes启用无磁盘复制,即直接从内存中往网络中发,避免全量同步时的磁盘IO。
2、Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO。
3、适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步。
4、限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力。
总结
Redis哨兵
master节点宕机。只能进行读操作,不能写。可用性下降。
解决方案:选择一个slave作为master。
哨兵的作用
检测集群的健康状况,及时选择新的master,保证系统的可用性。Sentinel也是集群。
1.监测
Sentinel 不断检查master和slave是否按预期工作。判断节点是否健康。
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。quorum可以在redis.conf配置文件中进行设置。
总结:
单个Sentinel节点认为某个Redis节点主观下线,可能不是真正的下线,网络阻塞也有可能。
当多个(多个>=qourum)Sentinel节点认为某个Redis节点主观下线,此时达到客观下线要求,Sentinel哨兵认为该Redis节点真的下线了,会进行一系列操作。(如主节点下线就进行故障转移)
2.自动故障恢复
2.1 选举新的master :Sentinel会将一个slave提升为master。
选举规则:
1、首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
2、然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
3、如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
4、最后是判断slave节点的运行id大小,越小优先级越高。
2.2 实现故障转移 :当故障实例恢复后也变为一个slave,也以新的master为主
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
1、sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
2、sentinel给所有其它slave发送slaveof (slave1的IP地址) 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
3、最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
3.通知
当集群发生故障转移时,会将最新信息推送给Redis的客户端。即作Redis为主节点、从节点的地址的通知者,发生变更后第一时间通知Redis客户端。
RedisTemplate的哨兵模式
步骤:
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置sentinel信息
spring:
redis:
sentinel:
master: mymaster
nodes: # 下面是yaml的列表写法,也可以是以","分割的字符串
- 192.168.182.160:27001
- 192.168.182.160:27002
- 192.168.182.160:27003
# 要记得给出Redis实例的密码,Sentinel只是根据需要执行的命令类型返回给我们对应的节点地址信息而已,该节点的密码还是要我们自己给出的。
# 没有密码可以不指定
password: Redis实例密码 # 由于不能指定多个密码,所以这里主从节点密码必须一致
3.配置主从读写分离
@Bean
// 自定义Lettuce连接配置类,指定Redis集群的主从节点读写分离策略,这里也没有讲默认的策略是什么
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
// lamda表达式写法
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
总结
读数据,simpleStringTemplate会连接所有的redis(主从节点)。读数据会任选一个节点读取数据;写数据,simpleStringTemplate选择master节点写入数据。
Redis的客户端必须感知这种变化,及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。哨兵进行主从切换之后,Java客户端要发现并获取最新的节点信息。得到最新的master。
Redis分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
1、海量数据存储问题
2、高并发写的问题
为解决上述问题使用分片集群
分片集群特征
1、集群中有多个master,每个master保存不同数据(分段存储,通过哈希实现),类似于60g的数据,分别分配到3台20g内存的服务器)
2、每个master都可以有多个slave节点
3、master之间通过ping监测彼此健康状态(代替哨兵Sentinel。分片集群自身具备故障转移等功能,可以不需要哨兵了)
4、客户端请求可以访问集群任意节点,最终都会被转发到正确节点(类似于路由的功能,通过插槽(hash slot)实现)
散列插槽
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上。
1.为什么数据的key与插槽绑定?
数据key不是与节点绑定,而是与插槽绑定。当要操作一个Key时,都是先计算其插槽值(hash slot),然后去寻找插槽区间,再根据插槽区间寻找对应的节点,然后重定向到该节点,对Key进行操作。
因为Redis节点会出现宕机、Redis集群后期可能会进行扩容节点等情况,都会导致Key直接与Redis节点进行绑定非常不稳定,而与插槽进行绑定,当节点出现宕机、集群扩容节点等情况,重新分配插槽区间给节点就好了,非常方便。
2.Redis如何计算计算插槽值?
redis会根据key的有效部分计算插槽值,分两种情况:
1、key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分
2、key中不包含“{}”,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16(应该是哈希算法的一种)算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
3.使用redis-cli -c 命令连接Redis客户端,不加 -c 参数无法进行节点自动重定向。
总结
1.Redis如何判断某个key应该在哪个实例?
将16384个插槽分配到不同的实例
根据key的有效部分计算哈希值,对16384取余
余数作为插槽,寻找插槽所在Redis实例即可
2.如何将同一类数据固定的保存在同一个Redis实例?
这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
如:商品包含手机、电脑、笔,那他们的key格式:{商品}商品id
集群伸缩
集群动态的增加或者删除节点。
Redis的伸缩功能命令 :redis-cli-cluster
添加节点
语法:
redis -cli --cluster 新Redis节点的ip地址:新Redis节点的端口号 想要添加的集群中的某一个节点ip地址:端口号 [可选参数]
详细:new_host:new_port 就是新增的节点,必须指定,而后面的 existing_host:existing_port 从目标集群中随便取一个节点地址就可以了,不管是哪个都可以,因为这个节点起的作用就是,把新增节点的消息通知给集群中每一个节点。
可选参数:
--cluster-slave:指定新增节点为从节点
--cluster-master-id:指定新增节点的主节点
注意事项:
1、要是不指定上面两个可选参数(--cluster-slave、--cluster-master-id),那么新增节点默认就是作为集群的一个主节点。
2、默认情况下,新增节点作为主节点添加以后,不会自动重新分配插槽区间给新增节点的,需要我们手动从别的主节点那里去获取。
案例:
需求:向集群中添加一个新的master节点,并向其中存储 num = 10
1、启动一个新的redis实例,端口为7004
2、添加7004到之前的集群,并作为一个master节点
3、给7004节点分配插槽,使得num这个key可以存储到7004实例这里需要两个新的功能:
1、添加一个节点到集群中
2、将部分插槽分配到新插槽
1、添加一个节点到集群中
·创建Redis实例
#创建文件夹
mkdir 7004
#拷贝配置文件
cp redis.conf 7004
#修改配置文件
sed -i s/6379/7004/g 7004/redis.conf
#启动Redis实例
redis-server 7004/redis.conf
·添加节点到Redis集群中
redis-cli --cluster add-node 192.168.182.160:7004 192.168.182.160:7001
·转移插槽
故障转移
分片集群同样具有哨兵的功能。
手动故障转移
在新的slave节点cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。
这种failover命令可以指定三种模式:
- 缺省:默认的流程
- force:强制,省略了对offset的一致性校验
- takeover:强制,直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
RedisTemplate访问分片集群
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置yml文件
spring:
redis:
cluster:
nodes: # 指定分片集群的每一个节点信息
- 192.168.182.160:7001
- 192.168.182.160:7002
- 192.168.182.160:7003
- 192.168.182.160:8001
- 192.168.182.160:8002
- 192.168.182.160:8003
3.配置读写分离
@Bean
// 自定义Lettuce连接配置类,指定Redis集群的主从节点读写分离策略,这里也没有讲默认的策略是什么
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
// lamda表达式写法
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}