Redis-持久化、主从集群、哨兵模式、分片集群、分布式缓存

高级篇 - 分布式缓存 Redis集群

0、单节点Redis的问题

  • 数据丢失问题

    Redis是内存存储,服务重启可能会丢失数据

    解决方案:利用Redis数据持久化,将数据写入磁盘

  • 并发能力问题

    单节点Redis并发能力虽然不错,单也无法满足如618这样的高并发场景

    解决方案:搭建主从集群,实现读写分离

  • 故障恢复问题

    如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段

    解决方案:利用Redis哨兵,实现健康检测和自动恢复

  • 存储能力问题

    Redis基于内存,单节点能存储的数据量难以满足海量数据需求

    解决方案:搭建分片集群,利用插槽机制实现动态扩容

一、Redis持久化

Redis有两种持久化,分别是RDB持久化与AOF持久化

1.1 RDB 持久化

1.1.1 基本介绍

RDB全称Redis Database Backup file (Redis数据备份文件),也被叫做Redis数据快照

简单来说就是把内存中的所有数据都记录到磁盘中。(保存在了当前目录)当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

快照文件称为RBD文件,默认保存在当前运行目录

image-20230710095530365

Redis怎么执行一下RBD文件?

  • 第一种

先使用下面命令连接Redis,然后再执行save命令,此时就会去执行RBD的备份操作了。

redis-cli
save

而这个执行的动作,是由Redis的主进程来完成的。而Redis是单线程,一旦执行了RDB,就不能执行其他动作了。

除此之外,RDB是把数据写入到磁盘,而磁盘IO往往是比较慢的,数据量比较大的话耗时比较久。

这种情况适合在Redis停机之前使用

如果是我们自己主动停机的时候,它会自动进行一次RDB。

也就是说默认就有持久化

image-20230710101055739

  • 第二种

连接客户端后执行bgsave命令。

这个保存的命令是在后台异步执行的,开启子进程执行RDB,避免主进程受到影响

redis-cli
bgsave

这种情况适合在Redis运行时做


如果我们想周期备份怎么办?

Redis内部有触发RDB机制,可以在redis.conf文件中找到,格式如下所示

image-20230710102450223


RBD的其他配置也可以在redis.conf文件中设置

image-20230710102716538

RDB文件保存在当前目录就是因为上图中的 dir./


1.1.2 RDB的fork原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据

完成fork后读取内存数据并写入RDB文件

因为是异步的,所以几乎可以做到对主进程零阻塞。

为什么是几乎?而不是完全?

因为读取内存数据写入RDB文件确实是异步执行的,但子进程的获取却不是,而是fork主进程得来的,而fork的过程是堵塞的,主线程只能做这个事,不能接收用户请求,因此我们必须加快fork的速度


fork底层是怎么实现的呢?

可以将物理内存理解为电脑中的内存条。Redis的主线程要实现对Redis的读写,也是在内存中操作。

在Linux系统中,所有的进程都没有办法直接操作物理内存,而是由操作系统给每个进程分配一个虚拟内存,主进程只能操作虚拟内存,而后操作系统会维护一个虚拟内存与物理内存之间的映射关系表,这个表称为页表

所以:主进程操作虚拟内存,而虚拟内存基于页表的映射关系到我们的物理内存真正的存储位置,这样就实现对物理内存数据的读写

image-20230710104913687

而我们fork的时候会创建一个子进程,他不是把内存中的数据做拷贝,仅仅是把页表做拷贝,当子进程有了和主进程一样的映射关系,子进程操作自己的虚拟内存时,会最终映射到相同的物理内存区域,这样实现了主进程与子进程之间内存数据共享,这样就无需拷贝内存中的数据,直接实现内存共享后速度就会变得非常的快,阻塞的时间就尽可能的缩短了

此时子进程就能大胆的读取物理内存中的数据了,把他写入磁盘当中的一个文件里面去。并且新的RBD文件会替换旧的RDB文件

异步持久化就实现了

image-20230710105441541

子进程写RDB的过程中,主进程依然可以修改内存中的数据

如果子进程读的时候主进程也正在写,此时读写之间可能会有冲突,甚至有可能会出现脏数据

为了避免这种情况的发生,fork采用copy-on-write技术

当我要去写的时候,我做一个拷贝,现在上图中是没有把数据做拷贝的,而是共享的,然后fork会把这个共享内存标记为read-only只读模式,任何一个进程都只能来读取数据而不能写数据

image-20230710110108572

那如果主进程接下来写怎么办?

他必须先拷贝一份数据(比如对数据B修改,就先拷贝一份数据B),再去完成写操作。并且之后主进程也在这个copy的数据里面读了,不再去read-only里面去了,也就是说主线程的页表映射关系发生了改变

image-20230710110437986

但是! 极端情况下,如果子进程写RDB文件的速度比较慢,并且主线程中有需求请求需要写入Redis,修改的数据也非常的多,也就是说要copy的数据也比较多,意味着Redis对于内存的占用要翻倍!

1.2.3 总结

RDB方式bgsave的基本流程

  • fork主进程得到一个子进程,共享内存空间
  • 子进程读取内存数据写入新的RDB文件
  • 用新RDB文件替换旧的RDB文件

RDB会在什么时候执行? save 60 1000代表什么含义?

  • 默认服务停止时
  • 代表60秒内至少执行1000次修改则触发RDB

RDB的缺点?

  • 安全漏洞问题。每隔60秒做一个持久化,但是60秒之间并没有做持久化,在这个过程当中产生的所有的写操作,一旦宕机就丢失。
  • fork子进程、压缩、写出RDB文件都比较耗时

1.2 AOF持久化

大大提高数据的安全性,弥补RDB的缺陷

AOF全称为Append Only File (追加文件)。

Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件

把Redis所有的写操作的命令记录到一个文件命令当中,这个文件中的内容是主键累加的过程

image-20230710111738925

如果将来Redis出现了问题,要恢复数据,就可以读取AOP文件,把里面的命令从到开始再执行一遍

AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

image-20230710111928433

AOF的命令记录的频率也可以通过redis.conf文件配置

image-20230710112049176

image-20230710112318006

因为是记录命令,AOF文件会比RDB文件大的多

而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义

比如下面对num三次操作,其实只有最后一次对我们有用,前两次每用。

但是恢复数据的时候,前两句也要执行,不是很合理

set num 123
set num 456
set name jack 
set num 789

通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果

bgrewriteaof

image-20230710114108159

执行bgrewriteaof命令后,我们就可以把set num 123、set num 456两条命令抛弃,只记录set name jack 、set num 789两条命令,并且可以把最后两条命令进行合并,因为都是set命令

这样以后AOF的体积与之前相比小了很多

image-20230710113759707

什么时候会执行bgrewriteaof命令,让文件执行重写功能呢?

redis.conf中可以配置一个触发值,自动去重写AOF文件。

image-20230710115853680

1.3 RDB与AOF对比

image-20230710140644400

数据恢复优先级:RDB与AOF均使用,同时有这两个文件,Redis在启动时会以谁优先?那当然是AOF,文件数据更完整

二、Redis主从集群

2.1 介绍

为什么需要主从架构

单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离

为什么要做成主从集群而不是负载均衡集群?

Redis应用中大多数是读多写少的场景。写操作我们在Master节点上操作,读操作在其他slave节点上操作

image-20230710141645740

2.2 搭建主从集群

来源黑马程序员

2.2.1 准备实例、配置

准备三个节点,一个主节点,两个从节点

image-20230710144123513

  • 创建目录

我们创建三个文件夹,名字分别叫7001、7002、7003:

# 进入/tmp目录
cd /tmp
# 创建目录
mkdir 7001 7002 7003

要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。

  • 恢复原始配置

修改之前redis.conf文件,将其中的持久化模式改为默认的RDB模式,AOF保持关闭状态

  • 拷贝配置文件到每个实例目录

拷贝配置文件到三个目录中,可以使用下面的命令:

#万式一: 逐个烤贝
cp redis-6.2.4/redis.conf 7001
cp redis-6.2.4/redis.conf 7002
cp redis-6.2.4/redis.conf 7003

#方式二:管道组合命令,一健烤贝
echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.4/redis.conf
  • 修改每个实例的端口、工作目录

修改配置文件的端口,分别为7001,7002,7003,将rdb文件保存位置都修改为自己所在目录(在/tmp目录执行下列命令)

sed -i -e 's/6379/7001/g' -e 's/dir .\//dir \/tmp\/7001\//g' 7001/redis.conf
sed -i -e 's/6379/7002/g' -e 's/dir .\//dir \/tmp\/7002\//g' 7002/redis.conf
sed -i -e 's/6379/7003/g' -e 's/dir .\//dir \/tmp\/7003\//g' 7003/redis.conf
  • 修改每个实例的声明IP

虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:

# redis实例的声明 IP
replica-announce-ip 192.168.150.101

每个目录都要改,我们一键完成修改(在/tmp目录执行下列命令):

# 逐一执行
sed -i '1a replica-announce-ip 192.168.150.101' 7001/redis.conf
sed -i '1a replica-announce-ip 192.168.150.101' 7002/redis.conf
sed -i '1a replica-announce-ip 192.168.150.101' 7003/redis.conf

# 或者一键修改
printf '%s\n' 7001 7002 7003 | xargs -I{
    
    } -t sed -i '1a replica-announce-ip 192.168.150.101' {
    
    }/redis.conf

2.2.2 启动

为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:

# 第1个
redis-server 7001/redis.conf
# 第2个
redis-server 7002/redis.conf
# 第3个
redis-server 7003/redis.conf

image-20230710150156626

如果要一键停止,可以运行下面命令:

printf '%s\n' 7001 7002 7003 | xargs -I{
    
    } -t redis-cli -p {
    
    } shutdown

2.2.3 开启主从关系

现在三个实例还没有任何关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。

有临时和永久两种模式

  • 修改配置文件(永久生效)

    • 在redis.conf中添加一行配置:slaveof <masterip> <masterport>

    指定master的ip和端口

  • 使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效)

    slaveof <masterip> <masterport>
    

注意:在5.0以后新增命令replicaof,与slaveof效果一致。

这里我们为了演示方便,使用方式二

通过redis-cli命令连接7002,执行下面命令:

# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 7001

通过redis-cli命令连接7003,执行下面命令:

# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001

然后连接 7001节点,查看集群状态:

# 连接 7001
redis-cli -p 7001
# 查看状态
info replication

总结

假设有A、B两个Redis实例,如何让B作为A的slave结点

在B节点执行命令:slaveof A的IP A的port

2.2.4 测试

执行下列操作以测试:

  • 利用redis-cli连接7001,执行set num 123

  • 利用redis-cli连接7002,执行get num,再执行set num 666

  • 利用redis-cli连接7003,执行get num,再执行set num 666

image-20230710151550501

可以发现,只有在7001这个master节点上可以执行写操作,7002和7003这两个slave节点只能执行读操作。

2.3 数据同步原理

Redis的主从之间已经实现了这种数据的同步

主从第一次同步是全量同步

但如果slave重启后同步,则执行增量同步

2.3.1 全量同步

第一阶段:判断一下是不是第一次

1.0 slave与master第一次建立连接的时候需要执行一个slaveof命令或replicaof命令,并且指定master的ip和端口,这个过程就是slave与master建立连接的过程

1.1 连接一旦建立,sleep就可以向master发送请求了,“你的数据给我一份”,目的是确保数据的一致性。

1.2此时master接收到请求后,master就做一个判断,判断slave是不是第一次请求

1.3如果是第一次请求同步数据的话,master向slave返回master的数据版本信息

1.4 slave接收到master的版本信息后,将其保存下来。将来可以基于数据版本做一个控制

image-20230710155802791


第二阶段

2.1 master怎么把所有数据发给slave?之前学过bgsave命令。此时会执行bgsave命令,生成RDB,一旦生成,里面记录了完整的内存信息

2.1.1bgsave命令在执行过程中(异步的),主进程会处理其他的写操作,新写的数据并不会发送给slave,而是master将RDB这段时间内的命令记录到repl_baklog缓冲区中

也就是说RDB文件中的数据外 + repl_baklog缓冲区中的数据 = 完整数据

2.2 master将RDB文件发送给slave

2.3 slave接收文件后,将本地的数据清空,加载RDB文件。确保master与slave节点数据的基本一致

image-20230710155813943


第三阶段

3.1 master发送repl_baklog中的命令到slave

3.2slave执行接收到的命令。此时保证master与slave节点数据完全一致

image-20230710160036343

总流程图

image-20230710160401512


这种同步什么叫全量同步

因为有一个RDB的过程,会把内存形成快照整体发送给slave,这种同步是比较消耗性能的,生成RDB文件的速度比较慢


master是怎么知道slave是第一次来呢?

先看两个概念:

  • Replication ld: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid

slave第一次请求master的时候,master会把自己的id给slave,id一样,说明是同一个数据集

  • offset:偏移量:随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的ofset。

    如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

也就是说offset越大,记录在repl_baklog里面的数据就越多

因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据

所以说了这么多,到底是怎么判断的?

offset>0,这么判断是不行的,因为不一定是从我们这个master同步过去的,也有可能从其他master同步的

基于Replication ld判断,如果不一样,就说明是第一次来

所以

1.1 slave向master发送请求申请数据同步的时候,需要携带Replication ld与offset

1.2判断是否是第一次同步时,比对一下Replication ld是否一致即可,如果不一致拒绝增量同步,开启全量同步

image-20230710161615054

2.3.2 增量同步

主从第一次同步是全量同步

但如果slave重启后同步,则执行增量同步


1.0 重启

在slave重启的过程中,数据肯定会落后于master,此时就需要我们去做一次同步

1.1 slave向master发送请求申请数据同步的时候,需要携带Replication ld与offset

1.2判断是否是第一次同步时,比对一下Replication ld是否一致即可,如果不一致拒绝增量同步,开启全量同步。

这个地方开启增量同步

1.3如果是第一次请求同步数据的话,master向slave返回master的数据版本信息;

如果不是第一次请求同步数据的话,恢复continue

下一步不用做RDB了,slave与master不同步的数据在repl_baklog中

image-20230710162910903


**2.1 **去repl_baklog中获取offset后的数据

**2.2 **master发送offset后的命令到slave

**2.3 **slave执行命令

image-20230710162930396


offset是记录的repl_baklog缓冲区的哪一个部分呢?怎么找到之后的那些命令的呢?

repl_baklog本质是一个数组。当数据记录满之后,会从0开始记录,把之前的数据覆盖掉(环型的一种记录方式)

image-20230710163251875

但是如果如果超过repl_baklog的存储上限的话(也就是红色把绿色覆盖后,slave跟不上master的进度了),那就没法做增量同步

如下图,slave宕机后无法做数据同步,master转了一圈追上slave,已经沾满了数组的空间。但是此时还不是最危险的

master还在记录新的命令,覆盖了一小部分绿色的,这还是正常的,没有什么危险

image-20230710163545097

下面是最危险的,已经出现问题了

master饶了一圈,到了自己的尾部,覆盖掉了一下slave还没有同步的命令

image-20230710163803845

repl_baklog大小有上限,写满后会覆盖最早的数据。

如果slave断开时间过久,导致尚未备份的数据被覆盖”则无法基于log做增量同步,只能再次全量同步

2.3.3 主从同步优化

总体思想:减少全量同步,优化全量同步的性能

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO

    正常的复制要生成RDB文件,我们就不生成了

    不把RDB文件写入到磁盘,而是写到网络当中,直接发送给slave,减少了一个磁盘读写

磁盘读取比较慢,但是网路特别快的时候使用

  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘I0

  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步

  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

还有一个是主节点同步的压力问题,如果slave节点非常多,都去找slave节点去做数据同步,就会给主节点造成很大的压力

image-20230710164817507

2.4 总结

全量同步和增量同步区别

  • 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。

  • 增量同步: slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave


什么时候执行全量同步?

  • slave节点第一次连接master节点时

  • slave节点断开时间太久,repl baklog中的offset已经被覆盖时


什么时候执行增量同步?

  • slave节点断开又恢复,并且在repl baklog中能找到offset时

三、哨兵模式

slave节点宕机恢复后可以找master节点同步数据?

那master节点宕机怎么办?

实时监控每个节点的状态,发现master宕机后立即选一个新的slave作为master

如果做了master节点的持久化,重启一下是没问题的,数据不会丢失。但是master挂机后,是无法执行写操作的,集群可用性下降了。

这个并不需要人工来做,有一个Redis哨兵机制,帮助我们完成整个集群的检测

3.1 哨兵的作用和原理

Redis提供哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的结构和作用如下

  • 监控

    Sentinel会不断检查您的master和slave是否按期工作

  • 自动故障恢复

    如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主

  • 通知

    Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

RedisClient需要连接各个Redis节点做读写分离,但是现在主节点挂了,哨兵然后做主从切换,那主从地址就变更了,但是java客户端并不知道这个事情

所谓的通知,就是我们的java客户端,他在找主从地址时不是直接去找Redis节点,而是去找Sentinel,由Sentinel告诉Redis的客户端主从的地址是什么。

将来主从发生了切换,Sentinel立即会将这个服务的状态变更通知客户端,那java客户端就知道谁是真的主,谁是真的从

image-20230711094316217


哨兵是如何得知集群中每个节点状态的呢?

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

主观下线: 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。

主观认为你下线了,但是可能没有下线。比如因为网络堵塞导致超时,未在规定时间响应

客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

指定数量(quorum):配置文件中的一个配置

image-20230711094811534


一旦发现master故障,sentinel需要在slave中选择一个新的master,选择依据如下

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds*10)则会排除该slave节点

down-after-milliseconds*10也是在配置文件中配置的。

超过指定值表示slave与master断开时间太长了,断开连接越长,丢失的数据越多,则排除该节点

  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举

  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高

  • 如果slave-prority与offset值相同,最后是判断slave节点的运行id大小,越小优先级越高


当选中了其中一个slave为新的master后(例如slave),故障的转移的步骤如下

  • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master

  • sentinel给所有其它slave发送slaveof 1921681501017002命令,让这些slave成为新master的从节点,开始从新的master上同步数据。

  • 最后,sentine[将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

image-20230711095916662

3.2 搭建哨兵集群

这里我们搭建三节点形成的Sentinel集群,来监管之前的Redis主从集群。

image-20230711100222469

三个sentinel实例信息如下:

节点 IP PORT
s1 192.168.150.101 27001
s2 192.168.150.101 27002
s3 192.168.150.101 27003

3.2.1 配置

要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。

我们创建三个文件夹,名字分别叫s1、s2、s3:

# 进入/tmp目录
cd /tmp
# 创建目录
mkdir s1 s2 s3

后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:

port 27001
sentinel announce-ip 192.168.150.101
sentinel monitor mymaster 192.168.150.101 7001 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/s1"

解读

  • port 27001:是当前sentinel实例的端口

  • sentinel announce-ip 192.168.150.101:声明一下自己的IP地址

  • sentinel monitor mymaster 192.168.150.101 7001 2:指定主节点信息

    sentinel monitor表示监控,mymaster 是给集群起的名字

    • mymaster:主节点名称,自定义,任意写
    • 192.168.150.101 7001:主节点的ip和端口

    那这样做只监控主节点Master不监控slave么?

    虽然我们监控的是master,但是在master上面可以得到集群中每个slave的信息的

    也就是说监控的是 7001端口为master的整个集群

    • 2:选举master时的quorum值
  • sentinel down-after-milliseconds mymaster 5000:

    与master断开的一个最长超时时间,不配置的话也有这个默认值,

  • sentinel failover-timeout mymaster 60000:

    slave故障恢复的超时时间,超时时间,不配置的话也有默认值,

  • dir "/tmp/s1":工作目录

然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/tmp目录执行下列命令):

# 方式一:逐个拷贝
cp s1/sentinel.conf s2
cp s1/sentinel.conf s3
# 方式二:管道组合命令,一键拷贝
echo s2 s3 | xargs -t -n 1 cp s1/sentinel.conf

修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:

sed -i -e 's/27001/27002/g' -e 's/s1/s2/g' s2/sentinel.conf
sed -i -e 's/27001/27003/g' -e 's/s1/s3/g' s3/sentinel.conf

3.2.2 启动

为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:

# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf

3.2.3 测试

尝试让master节点7001宕机,查看sentinel日志

image-20230711171146145

在我们停掉master7001的这一刻,Redis7002、7003,包括哨兵都有了变化

比如7002,7003在报错,因为连接不上主节点了

image-20230711171433011

而sentinel正在做一个选举

刚开始s1,s2,s3认为主观下线

image-20230711171707685

当三个都认为主观下线的时候,已经超过了选举master时的quorum值,由主观下线变成客观下线,然后7001就宕机了

image-20230711171925276

当master宕机后,sentinel要做一个try-failover处理,故障处理

image-20230711172207790

故障处理就是选出一个slave作为master

怎么选择下一个主节点?

  • 首先是哨兵之间(s1,s2,s3)要选择一个主节点(就是谁先发现的master宕机,谁就会选上)

image-20230711172524756

假如说s3选上了,那就要做故障恢复了

  • 再从slave中选择一个master

    s3在这里是找的7002

image-20230711172707516

  • 之后7002端口的redis执行slaveof-noone slave ,成为主节点

image-20230711172923713

然后在7002看一眼:首先是一直error一直连接不上,然后突然成为了master,

image-20230711173041307

  • 7002成为了一个新的主节点,然后需要把自己的信息广播给所有的从节点

    之前的7001主节点也要标记为从

    image-20230711173230158

    同样7003也会有操作

    image-20230711173305338

再观察一下7003端口,连接上后重新做一个全量同步

image-20230711173352837

3.3 RedisTemplate 连接哨兵

在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化及时更新连接信息。

Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换

3.3.1 配置

  • maven坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 在配置文件中配置application.yaml中指定sentinel相关信息

我们并不是配置的redis集群地址,而是sentinel地址

spring:
  redis:
    sentinel:
      master:  mymaster  # 指定master名称
      nodes: #指定redis-sentinel集群信息
        - 192.168.150.101:27001
        - 192.168.150.101:27002
        - 192.168.150,101:27003    

在sentinel模式下,我们的主从地址是有可能变更的,所以不能写死为某个redis的地址。

java客户端不需要知道redis集群的具体地址,只需要知道sentinel地址

java客户端能根据27001,27002,27003找到sentinel从而得知redis集群地址

  • 配置主从读写分离
    @Bean
    public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() {
    
    
//      是一个接口,不能直接new
        return new LettuceClientConfigurationBuilderCustomizer() {
    
    
           @Override
           public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
    
    
               clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
           }
       };
    }

或者使用lambda表达式

    @Bean
    public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() {
    
    
//      是一个接口,不能直接new
        return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
    }

这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:

  • MASTER:从主节点读取

  • MASTER PREFERRED:优先从master节点读取,master不可用才读取replica

  • REPLICA:从slave (replica)节点读取

  • REPLICA PREFERRED:优先从slave (replica)节点读取,所有的slave都不可用才读取master

四、Redis分片集群

主从集群能够应对Redis的高并发读的一个问题,但是Redis主从之间会做一个同步,为了提高主从同步时的一个性能,单节点的Redis的内存设置不要太高,如果内存占用的过多,在做RDB持久化时或者全量同步时就会导致IO性能有所下降。

如果说单节点Redis的内存降低了,存个10g,那有海量数据存储该怎么办?而且如果写的并发也很高,这该怎么办?

这些问题就需要Redis的分片集群来解决

4.1 分片集群结构

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决

  • 海量数据存储问题
  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征

  • 集群中有多个master,每个master保存不同的数据

每个master保存一部分数据,合起来就是总共的数据,这样的话能解决海量数据存储问题

此时Redis存储的上限取决于master节点的数量

  • 每个master都可以有多个slave节点

并发读的问题很好解决,再给master节点添加slave节点,即每个master本身还可以实现一个主从结构。

  • master之间通过ping检测彼此健康状态

之前做主从需要做一个哨兵的检测,但是现在不需要了,因为master互相之间就起到了一个哨兵的作用

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确的节点

将来节点之间会自动做一个路由,会把请求路由到正确的节点上,所以不再需要哨兵机制

image-20230714154502597

4.2 搭建分片集群

来自黑马程序员

这里我们会在同一台虚拟机中开启6个redis实例(三主三从),模拟分片集群,信息如下:

IP PORT 角色
192.168.150.101 7001 master
192.168.150.101 7002 master
192.168.150.101 7003 master
192.168.150.101 8001 slave
192.168.150.101 8002 slave
192.168.150.101 8003 slave

4.2.1 配置

删除之前的7001、7002、7003这几个目录,重新创建出7001、7002、7003、8001、8002、8003目录:

# 进入/tmp目录
cd /tmp
# 删除旧的,避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003

在/tmp下准备一个新的redis.conf文件,内容如下:

port 6379
# 开启集群功能
cluster-enabled yes
# 集群的配置文件名称,不需要我们创建,由redis自己维护
cluster-config-file /tmp/6379/nodes.conf
# 节点心跳失败的超时时间
cluster-node-timeout 5000
# 持久化文件存放目录
dir /tmp/6379 (6379可以改成对应的端口名)
# 绑定地址
bind 0.0.0.0
# 让redis后台运行
daemonize yes
# 注册的实例ip
replica-announce-ip 192.168.150.101
# 保护模式(不用做用户名和密码的校验了)
protected-mode no
# 数据库数量
databases 1
# 日志
logfile /tmp/6379/run.log

将这个文件拷贝到每个目录下:

# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf

修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:

# 进入/tmp目录
cd /tmp
# 修改配置文件
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{
    
    } -t sed -i 's/6379/{}/g' {
    
    }/redis.conf

4.2.2 启动

因为已经配置了后台启动模式,所以可以直接启动服务:

# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{
    
    } -t redis-server {
    
    }/redis.conf

通过ps查看状态:

ps -ef | grep redis

发现服务都已经正常启动:

image-20210702174255799

如果要关闭所有进程,可以执行命令:

ps -ef | grep redis | awk '{print $2}' | xargs kill

或者(推荐这种方式):

printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{
    
    } -t redis-cli -p {
    
    } shutdown

目前为止,只是把6个实例运行起来,但是他们6个之间的关系还没有说明(配置)

4.2.3 创建集群

虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。

我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。

  • Redis5.0之前

Redis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。

# 安装依赖
yum -y install zlib ruby rubygems
gem install redis

然后通过命令来管理集群:

# 进入redis的src目录
cd /tmp/redis-6.2.4/src
# 创建集群
./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003
  • Redis5.0以后

我们使用的是Redis6.2.4版本,集群管理以及集成到了redis-cli中,格式如下:

redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003

我们这么配置,怎么知道谁是master谁是slave呢?

判断依据就是replicas的数量是1,那master的数量也是1,如果是1主1从的话加起来是2(主从比例1:1)

那现在有6个节点,6÷2=3,则有3个master,3个slave。

那配置的六个结点中,前三个就是主(7001、7002、7003),后三个就是从(8001、8002、8003)

命令说明

  • redis-cli --cluster或者./redis-trib.rb:代表集群操作命令
  • create:代表是创建集群
  • --replicas 1或者--cluster-replicas 1 :指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas + 1) 得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master

image-20230714161007563

通过命令可以查看集群状态

redis-cli -p 7001 cluster nodes

image-20230714161119088

4.3 散列插槽

下面标红的slots插槽

image-20230714190414840

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:

image-20230714190721438

比如7001就分配了0~5460共5461个插槽


为什么要做这么一个插槽呢

假如我们要存储一个数据到集群里面,那这个数据应该存储在哪一个master上呢?并且不是随便存的,如果随便存的话,之后取数据也不是随便取的。插槽就是用来解决存与取的问题

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况

即数据key不是与master节点绑定的

  • key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
  • key中不包含“{}”,整个key都是有效部分

例如:key是num,那么根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。然后我们会知道某个节点中插槽的范围,那既然知道num插槽值,又知道节点插槽的范围,那我们就能确定把数据放在哪个节点了

操作任意一个插槽值,他就会先计算插槽值,再判断你在哪一个节点,完成一个请求的路由或重定向


为什么key与插槽绑定?

因为Redis的主节点可能出现宕机的情况,或者是集群扩容增加了节点,或者是集群伸缩删除节点都是都可能的。如果将某个节点删除或宕机后,绑定在节点的数据就丢失了

如果数据是跟插槽绑定的,那当该节点宕机时,可以将这个节点对应的插槽转移到活着的节点

集群扩容时,也可以将插槽进行转移,数据跟着插槽走,永远都能够找到数据所在的位置

比如下面我们set一个key为a,value为1,然后下面显示重定向到插槽15495,然后再Redis的7003节点

image-20230714203602379


假如我们在7003节点访问7001节点的数据怎么办

会重定向到7001节点的Redis,插槽编号是2765

image-20230714203834644

总结

  • Redis如何判断某个key应该在哪个实例

    ①创建集群时把16384个插槽分配到每个节点

    ②当我们取存储一个key或者是取一个key的时候,会根据key计算哈希值,再拿哈希值去计算插槽值

    ③插槽值计算出来以后就可以去判断这个槽在哪个节点上了,从而找到数据

根据key的有效部分计算哈希值,对16384取余数,余数作为插槽,寻找插槽所在实例即可

  • 如何将同一类数据固定的保存在同一个Redis实例

比如说相同的商品放到同一个节点之上,避免之后出现请求重定向。

因为请求重定向需要重新建立连接,他的性能上一定会有一定的损耗

    • 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀

那我们让其算出来的插槽是一样的,就一定会存在同一个实例。

插槽是一样,就代表key的有效部分要一样,也就是key要有一个共同的大括号

image-20230714205200120

4.4 集群伸缩

4.4.1 概念

作为分片集群,最重要的就是做集群的伸缩,也就是说集群必须能够动态的增加节点或者移除节点,这个内容就是集群的伸缩功能

添加节点

  • 参数new_host:new_port

    新节点ip和节点端口

  • existing_host:existing_port

    已经存在的主机ip和端口,也就是集群中已经有的主机ip和端口

为什么添加新节点的ip和端口还需要旧的呢?

因为向集群中添加一个节点,需要通知集群中的每一个角色,那我们得先联系上这个集群

image-20230714210905635

–cluster-slave

–cluster-master-id

这两个参数默认是没有加的,在没加这两个参数的话,我们新增的这个节点默认就是一个master节点。

如果我们添加了–cluster-slave参数,就会变成一个从节点,并且还能指定–cluster-master-id主节点是谁

4.4.2 案例

向集群中添加一个新的master节点,并向其中存储 num = 10

num算出来的插槽是在7001上面的,现在新增了一个7004,我们现在想把num存入到7004上,也就意味着要把7001的插槽分配到7004上

难点:插槽分配的问题

需求

  • 启动一个新的redis实例,端口为7004

参照搭建分配集群进行创建即可

在tmp目录下执行下面命令:

mkdir 7004
cp redis.conf 7004
sed -i s/6379/7004/g 7004/redis.conf

配置好后运行

redis-server 7004/redis.conf

查看是否成功启动

ps -ef | grep redis

我们目前只是启动了,并没有成为集群中的一个节点

image-20230714212739877

  • 添加7004到之前的集群,并作为一个master节点
redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001

之后查看一下,新添加的7004master节点并没有插槽

image-20230714213036635

  • 给7004节点分配插槽,使得num这个key可以存储到7004实例

我们先看一下key为num的节点在7001,并且插槽是2675,现在我们要把插槽分配给7004即可

image-20230714213151882

怎么做插槽分配?

使用reshard命令,然后再给一个集群中的某一个ip与端口

image-20230714213326009

redis-cli --cluster reshard 192.168.150.101:7001

然后他就会问你,你想移动多少个插槽?

image-20230714213541415

因为num的插槽是2675,那我们移动的数量大于2675即可,比如给一个3000

然后会问你,谁会接收这一部分插槽?然后写上7004的id即可

image-20230714213733775

然后接着会问你,这个槽或从哪里作为数据源进行拷贝?

我们这个地方是从7001进行拷贝,写上7001的id即可

image-20230714213856068

之后就从7001拷贝到7002

这个地方完成后我们输入“done”即可,代表完成了

image-20230714213941098

回车后会继续问你,要不要将3000个插槽移动过去? 我们回答“yes”即可,然后就开始移动

image-20230714214147988

4.5 故障转移

分片集群虽然没有哨兵机制,但是也具备故障专业功能

4.5.1 自动故障转移

当集群中有一个master宕机,会发生什么?

①某一个主节点失去连接

②做一个心跳检测,此节点失去连接后,会被标记成一个失败的状态

image-20230714214954252

③确定下线后,自动提升一个slave为新的master


演示

如今master节点有7001,7002,7003

使用如下命令监控集群状态

redis-cli -p 7001 cluster nodes

image-20230714214407260

让7002节点宕机

redis-cli -p 7002 shutdown

之后8003变成了master了,7002连接失败

image-20230714214612487

再启动7002

redis-server 7002/redis.conf

此时7002变成了slave

image-20230714214742938

上面的演示我们并不需要哨兵,redis集群自动具备主从故障切换这种功能

4.5.2 手动故障专业

为什么要做手动的故障转移呢?

比如7001是一个master节点,但是机器故障老旧,需要做维护,可以启动一个新的节点作为7001的slave,然后手动的让新结点替换7001master节点,实现手动故障转移

怎么做呢?

首先要有一个新的子节点slave,然后需要在新的子节点执行cluster failover命令。执行完后slave节点对应的那个master节点就会被替换掉,之前的master变成了slave

image-20230714221459315

手动的Failover支持三种不同模式

  • 缺省:默认的流程,上面1-6步

缺省就是什么都不写,都是默认参数

  • force:省略了对offset的一致性校验

也就是把二、三步骤省略掉

  • takeover:直接执行第五步,忽略数据一致性、忽略master状态和其他master意见

案例:在7002这个slave节点执行手动故障转移,重新夺回master地位

步骤如下:

  • 利用redis-cli连接7002这个节点
redis-cli -p 7002
  • 执行cluster failover命令
cluster failover

image-20230714222543360

4.6 RedisTemplate 访问分片集群

看一看与哨兵模式有什么差别

RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:

哨兵模式中,1和3都做了,差别就在2配置这个地方

1.引入redis的starter依赖

2.配置分片集群地址

3.配置读写分离

与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:

之前是配置的哨兵的地址,现在我们是配置分片集群中每一个节点的信息

spring:
  redis:
    cluster:
      nodes: # 指定分片集群的每一个节点信息
        - 192.168.150.101:7001
        - 192.168.150.101:7002
        - 192.168.150.101:7003
        - 192.168.15.101:8001
        - 192.168.150.101:8002
        - 192.168.150.101:8003

猜你喜欢

转载自blog.csdn.net/weixin_51351637/article/details/131744537