文章目录
- [高级篇 - 分布式缓存 Redis集群](#高级篇 - 分布式缓存 Redis集群)
- 0、单节点Redis的问题
- 一、Redis持久化
-
- [1.1 RDB 持久化](#1.1 RDB 持久化)
-
- [1.1.1 基本介绍](#1.1.1 基本介绍)
- [1.1.2 RDB的fork原理](#1.1.2 RDB的fork原理)
- [1.2.3 总结](#1.2.3 总结)
- [1.2 AOF持久化](#1.2 AOF持久化)
- [1.3 RDB与AOF对比](#1.3 RDB与AOF对比)
- 二、Redis主从集群
-
- [2.1 介绍](#2.1 介绍)
- [2.2 搭建主从集群](#2.2 搭建主从集群)
-
- [2.2.1 准备实例、配置](#2.2.1 准备实例、配置)
- [2.2.2 启动](#2.2.2 启动)
- [2.2.3 开启主从关系](#2.2.3 开启主从关系)
- [2.2.4 测试](#2.2.4 测试)
- [2.3 数据同步原理](#2.3 数据同步原理)
-
- [2.3.1 全量同步](#2.3.1 全量同步)
- [2.3.2 增量同步](#2.3.2 增量同步)
- [2.3.3 主从同步优化](#2.3.3 主从同步优化)
- [2.4 总结](#2.4 总结)
- 三、哨兵模式
-
- [3.1 哨兵的作用和原理](#3.1 哨兵的作用和原理)
- [3.2 搭建哨兵集群](#3.2 搭建哨兵集群)
-
- [3.2.1 配置](#3.2.1 配置)
- [3.2.2 启动](#3.2.2 启动)
- [3.2.3 测试](#3.2.3 测试)
- [3.3 RedisTemplate 连接哨兵](#3.3 RedisTemplate 连接哨兵)
-
- [3.3.1 配置](#3.3.1 配置)
- 四、Redis分片集群
-
- [4.1 分片集群结构](#4.1 分片集群结构)
- [4.2 搭建分片集群](#4.2 搭建分片集群)
-
- [4.2.1 配置](#4.2.1 配置)
- [4.2.2 启动](#4.2.2 启动)
- [4.2.3 创建集群](#4.2.3 创建集群)
- [4.3 散列插槽](#4.3 散列插槽)
- [4.4 集群伸缩](#4.4 集群伸缩)
-
- [4.4.1 概念](#4.4.1 概念)
- [4.4.2 案例](#4.4.2 案例)
- [4.5 故障转移](#4.5 故障转移)
-
- [4.5.1 自动故障转移](#4.5.1 自动故障转移)
- [4.5.2 手动故障专业](#4.5.2 手动故障专业)
- [4.6 RedisTemplate 访问分片集群](#4.6 RedisTemplate 访问分片集群)
高级篇 - 分布式缓存 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文件,默认保存在当前运行目录
Redis怎么执行一下RBD文件?
- 第一种
先使用下面命令连接Redis,然后再执行save命令,此时就会去执行RBD的备份操作了。
redis-cli
save
而这个执行的动作,是由Redis的主进程来完成的。而Redis是单线程,一旦执行了RDB,就不能执行其他动作了。
除此之外,RDB是把数据写入到磁盘,而磁盘IO往往是比较慢的,数据量比较大的话耗时比较久。
这种情况适合在Redis停机之前使用
如果是我们自己主动停机的时候,它会自动进行一次RDB。
也就是说默认就有持久化
- 第二种
连接客户端后执行bgsave命令。
这个保存的命令是在后台异步执行的,开启子进程执行RDB,避免主进程受到影响
redis-cli
bgsave
这种情况适合在Redis运行时做
如果我们想周期备份怎么办?
Redis内部有触发RDB机制,可以在redis.conf文件中找到,格式如下所示

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

RDB文件保存在当前目录就是因为上图中的 dir./
1.1.2 RDB的fork原理
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。
完成fork后读取内存数据并写入RDB文件。
因为是异步的,所以几乎可以做到对主进程零阻塞。
为什么是几乎?而不是完全?
因为读取内存数据写入RDB文件确实是异步执行的,但子进程的获取却不是,而是fork主进程得来的,而fork的过程是堵塞的,主线程只能做这个事,不能接收用户请求,因此我们必须加快fork的速度
fork底层是怎么实现的呢?
可以将物理内存理解为电脑中的内存条。Redis的主线程要实现对Redis的读写,也是在内存中操作。
在Linux系统中,所有的进程都没有办法直接操作物理内存,而是由操作系统给每个进程分配一个虚拟内存,主进程只能操作虚拟内存,而后操作系统会维护一个虚拟内存与物理内存之间的映射关系表,这个表称为页表
所以:主进程操作虚拟内存,而虚拟内存基于页表的映射关系到我们的物理内存真正的存储位置,这样就实现对物理内存数据的读写

而我们fork的时候会创建一个子进程,他不是把内存中的数据做拷贝,仅仅是把页表做拷贝,当子进程有了和主进程一样的映射关系,子进程操作自己的虚拟内存时,会最终映射到相同的物理内存区域,这样实现了主进程与子进程之间内存数据共享,这样就无需拷贝内存中的数据,直接实现内存共享后速度就会变得非常的快,阻塞的时间就尽可能的缩短了
此时子进程就能大胆的读取物理内存中的数据 了,把他写入磁盘当中的一个文件里面去。并且新的RBD文件会替换旧的RDB文件
异步持久化就实现了

子进程写RDB的过程中,主进程依然可以修改内存中的数据
如果子进程读的时候主进程也正在写,此时读写之间可能会有冲突,甚至有可能会出现脏数据
为了避免这种情况的发生,fork采用copy-on-write技术
当我要去写的时候,我做一个拷贝,现在上图中是没有把数据做拷贝的,而是共享的,然后fork会把这个共享内存标记为read-only只读模式,任何一个进程都只能来读取数据而不能写数据

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

但是! 极端情况下,如果子进程写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所有的写操作的命令记录到一个文件命令当中,这个文件中的内容是主键累加的过程

如果将来Redis出现了问题,要恢复数据,就可以读取AOP文件,把里面的命令从到开始再执行一遍
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

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


因为是记录命令,AOF文件会比RDB文件大的多。
而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。
比如下面对num三次操作,其实只有最后一次对我们有用,前两次每用。
但是恢复数据的时候,前两句也要执行,不是很合理
set num 123
set num 456
set name jack
set num 789
通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
bgrewriteaof

执行bgrewriteaof命令后,我们就可以把set num 123、set num 456两条命令抛弃,只记录set name jack 、set num 789两条命令,并且可以把最后两条命令进行合并,因为都是set命令
这样以后AOF的体积与之前相比小了很多

什么时候会执行bgrewriteaof命令,让文件执行重写功能呢?
redis.conf中可以配置一个触发值,自动去重写AOF文件。

1.3 RDB与AOF对比

数据恢复优先级:RDB与AOF均使用,同时有这两个文件,Redis在启动时会以谁优先?那当然是AOF,文件数据更完整
二、Redis主从集群
2.1 介绍
为什么需要主从架构?
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
为什么要做成主从集群而不是负载均衡集群?
Redis应用中大多数是读多写少的场景。写操作我们在Master节点上操作,读操作在其他slave节点上操作

2.2 搭建主从集群
来源黑马程序员
2.2.1 准备实例、配置
准备三个节点,一个主节点,两个从节点

- 创建目录
我们创建三个文件夹,名字分别叫7001、7002、7003:
sh
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir 7001 7002 7003
要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。
- 恢复原始配置
修改之前redis.conf文件,将其中的持久化模式改为默认的RDB模式,AOF保持关闭状态
- 拷贝配置文件到每个实例目录
拷贝配置文件到三个目录中,可以使用下面的命令:
sh
#万式一: 逐个烤贝
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目录执行下列命令)
sh
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信息,格式如下:
properties
# redis实例的声明 IP
replica-announce-ip 192.168.150.101
每个目录都要改,我们一键完成修改(在/tmp目录执行下列命令):
sh
# 逐一执行
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实例,启动命令:
sh
# 第1个
redis-server 7001/redis.conf
# 第2个
redis-server 7002/redis.conf
# 第3个
redis-server 7003/redis.conf

如果要一键停止,可以运行下面命令:
sh
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.conf中添加一行配置:
-
使用redis-cli客户端连接到redis服务,执行slaveof命令(重启后失效):
shslaveof <masterip> <masterport>
注意:在5.0以后新增命令replicaof,与slaveof效果一致。
这里我们为了演示方便,使用方式二。
通过redis-cli命令连接7002,执行下面命令:
sh
# 连接 7002
redis-cli -p 7002
# 执行slaveof
slaveof 192.168.150.101 7001
通过redis-cli命令连接7003,执行下面命令:
sh
# 连接 7003
redis-cli -p 7003
# 执行slaveof
slaveof 192.168.150.101 7001
然后连接 7001节点,查看集群状态:
sh
# 连接 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

可以发现,只有在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的版本信息后,将其保存下来。将来可以基于数据版本做一个控制

第二阶段
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节点数据的基本一致

第三阶段
3.1 master发送repl_baklog中的命令到slave
3.2slave执行接收到的命令。此时保证master与slave节点数据完全一致

总流程图

这种同步什么叫全量同步?
因为有一个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是否一致即可,如果不一致拒绝增量同步,开启全量同步

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中

**2.1 **去repl_baklog中获取offset后的数据
**2.2 **master发送offset后的命令到slave
**2.3 **slave执行命令

offset是记录的repl_baklog缓冲区的哪一个部分呢?怎么找到之后的那些命令的呢?
repl_baklog本质是一个数组。当数据记录满之后,会从0开始记录,把之前的数据覆盖掉(环型的一种记录方式)

但是如果如果超过repl_baklog的存储上限的话(也就是红色把绿色覆盖后,slave跟不上master的进度了),那就没法做增量同步
如下图,slave宕机后无法做数据同步,master转了一圈追上slave,已经沾满了数组的空间。但是此时还不是最危险的
master还在记录新的命令,覆盖了一小部分绿色的,这还是正常的,没有什么危险

下面是最危险的,已经出现问题了
master饶了一圈,到了自己的尾部,覆盖掉了一下slave还没有同步的命令

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节点去做数据同步,就会给主节点造成很大的压力

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客户端就知道谁是真的主,谁是真的从

哨兵是如何得知集群中每个节点状态的呢?
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线: 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
主观认为你下线了,但是可能没有下线。比如因为网络堵塞导致超时,未在规定时间响应
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
指定数量(quorum):配置文件中的一个配置

一旦发现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节点

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

三个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:
sh
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir s1 s2 s3
后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:
ini
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目录执行下列命令):
sh
# 方式一:逐个拷贝
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:
sh
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实例,启动命令:
sh
# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf
3.2.3 测试
尝试让master节点7001宕机,查看sentinel日志:

在我们停掉master7001的这一刻,Redis7002、7003,包括哨兵都有了变化
比如7002,7003在报错,因为连接不上主节点了
而sentinel正在做一个选举
刚开始s1,s2,s3认为主观下线
当三个都认为主观下线的时候,已经超过了选举master时的quorum值,由主观下线变成客观下线,然后7001就宕机了
当master宕机后,sentinel要做一个try-failover处理,故障处理

故障处理就是选出一个slave作为master
怎么选择下一个主节点?
- 首先是哨兵之间(s1,s2,s3)要选择一个主节点(就是谁先发现的master宕机,谁就会选上)

假如说s3选上了,那就要做故障恢复了
-
再从slave中选择一个master
s3在这里是找的7002

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

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

-
7002成为了一个新的主节点,然后需要把自己的信息广播给所有的从节点
之前的7001主节点也要标记为从
同样7003也会有操作
再观察一下7003端口,连接上后重新做一个全量同步
3.3 RedisTemplate 连接哨兵
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化及时更新连接信息。
Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换
3.3.1 配置
- maven坐标
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 在配置文件中配置application.yaml中指定sentinel相关信息
我们并不是配置的redis集群地址,而是sentinel地址
yaml
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集群地址
- 配置主从读写分离
java
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() {
// 是一个接口,不能直接new
return new LettuceClientConfigurationBuilderCustomizer() {
@Override
public void customize(LettuceClientConfiguration.LettuceClientConfigurationBuilder clientConfigurationBuilder) {
clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
};
}
或者使用lambda表达式
java
@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互相之间就起到了一个哨兵的作用
- 客户端请求可以访问集群任意节点,最终都会被转发到正确的节点
将来节点之间会自动做一个路由,会把请求路由到正确的节点上,所以不再需要哨兵机制

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目录:
sh
# 进入/tmp目录
cd /tmp
# 删除旧的,避免配置干扰
rm -rf 7001 7002 7003
# 创建目录
mkdir 7001 7002 7003 8001 8002 8003
在/tmp下准备一个新的redis.conf文件,内容如下:
ini
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
将这个文件拷贝到每个目录下:
sh
# 进入/tmp目录
cd /tmp
# 执行拷贝
echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf
修改每个目录下的redis.conf,将其中的6379修改为与所在目录一致:
sh
# 进入/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 启动
因为已经配置了后台启动模式,所以可以直接启动服务:
sh
# 进入/tmp目录
cd /tmp
# 一键启动所有服务
printf '%s\n' 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf
通过ps查看状态:
sh
ps -ef | grep redis
发现服务都已经正常启动:

如果要关闭所有进程,可以执行命令:
sh
ps -ef | grep redis | awk '{print $2}' | xargs kill
或者(推荐这种方式):
sh
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环境。
sh
# 安装依赖
yum -y install zlib ruby rubygems
gem install redis
然后通过命令来管理集群:
sh
# 进入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中,格式如下:
sh
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

通过命令可以查看集群状态:
sh
redis-cli -p 7001 cluster nodes

4.3 散列插槽
下面标红的slots插槽

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

比如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节点

假如我们在7003节点访问7001节点的数据怎么办?
会重定向到7001节点的Redis,插槽编号是2765

总结
-
Redis如何判断某个key应该在哪个实例?
①创建集群时把16384个插槽分配到每个节点
②当我们取存储一个key或者是取一个key的时候,会根据key计算哈希值,再拿哈希值去计算插槽值
③插槽值计算出来以后就可以去判断这个槽在哪个节点上了,从而找到数据
根据key的有效部分计算哈希值,对16384取余数,余数作为插槽,寻找插槽所在实例即可
- 如何将同一类数据固定的保存在同一个Redis实例?
比如说相同的商品放到同一个节点之上,避免之后出现请求重定向。
因为请求重定向需要重新建立连接,他的性能上一定会有一定的损耗
-
- 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
那我们让其算出来的插槽是一样的,就一定会存在同一个实例。
插槽是一样,就代表key的有效部分要一样,也就是key要有一个共同的大括号

4.4 集群伸缩
4.4.1 概念
作为分片集群,最重要的就是做集群的伸缩,也就是说集群必须能够动态的增加节点或者移除节点,这个内容就是集群的伸缩功能
添加节点
-
参数new_host:new_port
新节点ip和节点端口
-
existing_host:existing_port
已经存在的主机ip和端口,也就是集群中已经有的主机ip和端口
为什么添加新节点的ip和端口还需要旧的呢?
因为向集群中添加一个节点,需要通知集群中的每一个角色,那我们得先联系上这个集群

--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目录下执行下面命令:
sh
mkdir 7004
cp redis.conf 7004
sed -i s/6379/7004/g 7004/redis.conf
配置好后运行
sh
redis-server 7004/redis.conf
查看是否成功启动
sh
ps -ef | grep redis
我们目前只是启动了,并没有成为集群中的一个节点

- 添加7004到之前的集群,并作为一个master节点
sh
redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001
之后查看一下,新添加的7004master节点并没有插槽

- 给7004节点分配插槽,使得num这个key可以存储到7004实例
我们先看一下key为num的节点在7001,并且插槽是2675,现在我们要把插槽分配给7004即可

怎么做插槽分配?
使用reshard命令,然后再给一个集群中的某一个ip与端口

sh
redis-cli --cluster reshard 192.168.150.101:7001
然后他就会问你,你想移动多少个插槽?

因为num的插槽是2675,那我们移动的数量大于2675即可,比如给一个3000
然后会问你,谁会接收这一部分插槽?然后写上7004的id即可

然后接着会问你,这个槽或从哪里作为数据源进行拷贝?
我们这个地方是从7001进行拷贝,写上7001的id即可

之后就从7001拷贝到7002
这个地方完成后我们输入"done"即可,代表完成了

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

4.5 故障转移
分片集群虽然没有哨兵机制,但是也具备故障专业功能
4.5.1 自动故障转移
当集群中有一个master宕机,会发生什么?
①某一个主节点失去连接
②做一个心跳检测,此节点失去连接后,会被标记成一个失败的状态

③确定下线后,自动提升一个slave为新的master
演示
如今master节点有7001,7002,7003
使用如下命令监控集群状态
sh
redis-cli -p 7001 cluster nodes

让7002节点宕机
sh
redis-cli -p 7002 shutdown
之后8003变成了master了,7002连接失败

再启动7002
sh
redis-server 7002/redis.conf
此时7002变成了slave

上面的演示我们并不需要哨兵,redis集群自动具备主从故障切换这种功能
4.5.2 手动故障专业
为什么要做手动的故障转移呢?
比如7001是一个master节点,但是机器故障老旧,需要做维护,可以启动一个新的节点作为7001的slave,然后手动的让新结点替换7001master节点,实现手动故障转移
怎么做呢?
首先要有一个新的子节点slave,然后需要在新的子节点执行cluster failover命令。执行完后slave节点对应的那个master节点就会被替换掉,之前的master变成了slave

手动的Failover支持三种不同模式:
- 缺省:默认的流程,上面1-6步
缺省就是什么都不写,都是默认参数
- force:省略了对offset的一致性校验
也就是把二、三步骤省略掉
- takeover:直接执行第五步,忽略数据一致性、忽略master状态和其他master意见
案例:在7002这个slave节点执行手动故障转移,重新夺回master地位
步骤如下:
- 利用redis-cli连接7002这个节点
sh
redis-cli -p 7002
- 执行cluster failover命令
sh
cluster failover

4.6 RedisTemplate 访问分片集群
看一看与哨兵模式有什么差别
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
哨兵模式中,1和3都做了,差别就在2配置这个地方
1.引入redis的starter依赖
2.配置分片集群地址
3.配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
之前是配置的哨兵的地址,现在我们是配置分片集群中每一个节点的信息
yaml
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