目录
单点Redis
单点Redis存在如下问题:
- Redis是内存存储,服务重启可能会造成数据丢失。
- 并发问题,虽然是内存存储,并发能力很强,但在单节点下,不适用于高并发的场景。
- 故障恢复问题,Redis服务宕机,导致某些服务不可用。需要一种自动恢复的手段。
- 存储能力问题,Redis基于内存,单节点的存储数量难以满足海量数据需求。
对应的解决方案:
- 数据丢失问题:实现Redis数据持久化
- 并发能力问题:搭建主从集群,实现读写分离
- 故障恢复问题:利用Redis哨兵,实现健康和自动恢复
- 存储能力问题:搭建分片集群,利用插槽机制实现动态扩容
Redis数据持久化
一共有两种持久化方式:RDB与AOF
RDB持久化
简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件成为RDB文件,默认保存在当前运行目录。
执行命令为
save
该命令由Redis主进程去执行,但由于Redis是单线程的,因此在持久化期间,其他所有命令都会被阻塞。一般不推荐这种方式。而推荐下面这个命令
bgsave
开启子进程执行RDB,不影响主进程。
Redis在主动停机前,会自动执行一次RDB。
但是在Redis内部数据比较多的时候,RDB时间可能会很久,在save期间,如果服务宕机,仍然可能导致数据丢失。因此我们可以在配置文件中修改RDB触发机制。
需要注意的是,以上save实际上都是bgsave,如果是 save "",则代表禁用RDB。
其他配置如下
修改RDB触发条件为5秒内至少有一个key被修改后,重启Redis服务,再进行一个添加操作观察redis服务器,会自动进行RDB。
bgsave细节
bgsave是开启一个子进程去对数据进行持久化操作,虽然实现了异步持久化,但是在fork主进程得到子进程期间是一个阻塞式的操作,为了减少阻塞时间,fork底层实现如下。
主进程是无法直接对物理内存进行操作的,开启主进程时,操作系统会对主进程分配一个虚拟内存,并维护页表,而页表中记录了虚拟内存与物理内存的映射关系,主进程开启子进程过程中仅仅是拷贝了一个页表,并不是拷贝了内存中的数据给子进程去做持久化。因此,主进程和子进程实际上共享同一个内存。
共享同一个内存也存在一个缺点,就是在RDB过程中,主进程需要对数据进行修改。这样就造成了读写冲突。为此,fork底层采用了copy-on-write技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作。
修改数据前,将原有数据拷贝一份后再进行写操作,同时将主进程中的页表映射关系进行修改。
RDB的缺点
- RDB执行间隔时间长,两次RDB之间写入数据有丢失风险。
- fork子进程,压缩,写出RDB文件都比较耗时。
AOF持久化
Redis处理的每一个写命令都会记录在AOF文件中,可以看做命令日志文件
当服务重启后,会从AOF文件中将所有命令再执行一边。而AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF
三种刷盘机制对比
|----------|--------|--------------|----------------|
| 配置项 | 刷盘时机 | 优点 | 缺点 |
| Always | 同步 | 可靠性高,几乎不丢失数据 | 性能影响较大 |
| everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
| no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
开启AOF功能后,执行一次写操作,观察AOF文件
重启Redis服务,会进行一次DB加载
AOF的问题
AOF会记录所有的写操作,但是对于同一个key,记录多次set操作是无意义的,只需要最后一次的set值就满足了,如果执行了delete操作,那么之前的set操作也无意义。因此可以通过执行bgrewriteaof命令,对AOF文件进行重写。可以通过修改配置文件来控制重写时机
RDB与AOF对比
|---------|------------------------|----------------------------------|
| | RDB | AOF |
| 持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
| 数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
| 文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
| 宕机恢复速度 | 很快 | 慢 |
| 数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
| 系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源 但AOF重写时会占用大量CPU和内存资源 |
| 使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高常见 |
搭建Redis主从架构
Reids搭建集群主要是为了实现读写分离,由于大多数使用Redis做缓存,因此是读多写少,也就是说,主节点去实现写操作,从节点去实现读操作。
接下来我们在同一台虚拟机上创建3个Redis实例,实现主从集群
bash
#创建3个目录,分别存放不同启动端口的Redis实例
cd /tmp
mkdir 7001 7002 7003
#将redis中的redis.conf文件拷贝到这三个文件当中
# 方式一:逐个拷贝
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
# 修改配置文件中的启动端口以及文件保存位置
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
# 逐一执行
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
#启动
redis-server 7001/redis.conf
redis-server 7002/redis.conf
redis-server 7003/redis.conf
接下来搭建主从关系:
临时方式(重启失效)
客户短连接redis服务后,执行slaveof方法
bash
slaveof 主节点ip 主节点端口
永久方式:修改配置文件添加如下配置
bash
slaveof 主节点ip 主节点端口
连接后主节点会打印节点同步信息。将7002、7003将7001作为主节点后,输入如下命令查看集群信息
bash
info replication
测试是否可以同步信息,在7001加入数据,在7002查询数据(在从节点无法写入数据)
数据同步原理
全量同步
主从第一次同步也叫全量同步,具体流程如下
解释:当从节点发起数据同步请求时,主节点会判断该节点是否是第一次进行数据同步,如果是第一次,主节点会返回自己数据的版本信息给从节点保存,同时执行一个bgsave操作,去生成RDB文件后发送给从节点,在生成RDB文件期间会将所有的写操作保存在repl_baklog命令缓冲区。从节点接收到RDB文件后,会清空自身数据后加载RDB文件,加载完成后,主节点会将缓冲区的所有命令发送给从节点去执行,从而保证主从信息保持一致。
master如何判断slave是不是第一次同步数据?
Replication Id:简称replid,是数据集的标记,id一致说明是同一数据集,每一个master都存在唯一的replid,而slave会继承master的id用来识别主从节点属于同一数据集。
offset:偏移量。随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
slave做数据同步时,必须向master声明自己的replication id(用来判断是否使用同一个数据集)和offset(用来判断同一个数据集下的同步进度),master才会知道有哪些数据需要同步。
因此第一阶段就变成如下流程
查看Redis服务器打印信息
增量同步
当slave宕机重启后,再次与主节点连接时,进行的是增量同步(局部同步)
解释:slave发送自己的数据集id和偏移量信息给主节点,主节点判断不是第一次连接后,就进行增量同步。将repl_baklog命令缓冲区获取自身偏移量与主节点记录的偏移量之间的数据。
由于repl_baklog的文件大小固定,当写满后,会覆盖最早的数据(可以理解为环形数组)。如果slave断开过久,导致未备份的数据被覆盖,则无法基于repl_baklog做增量同步,只能进行全量同步。
主从同步优化
由于全量同步耗时比较久,因此我们要尽可能的减少Redis进行全量同步的次数
- 在主节点的配置文件中配置
repl-dishless-sync yes
启用无磁盘复制,避免全量同步时的磁盘IO。简单来说就是在写RDB文件时,不写入磁盘,而是通过网络IO流直接写给从节点(适用于网络带宽快的场景) - Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO(数据少,那么IO流传输数据就少)
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
Redis哨兵
Redis哨兵(Sentinel)机制来实现主从集群的自动故障恢复,结构如下:
Redis哨兵作用如下:
- 监控:Sentinel会不断检查master和slave是否按预期工作。
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
集群检测
Redis哨兵通常集群搭建,基于心跳检测来监控所有节点的状态,每隔一秒向集群每个节点发送ping,如果超过时间没有接收到响应,则认定为主观下线,如果Redis哨兵集群超过指定数量(建议是节点数量的一半)的节点都没有接收到响应,则认定为客观下线(真的下线了),移除下线节点,如果是主节点宕机,需要及时选举新的主节点。其次就是通知java客户端,告知客户端去访问哪个节点。
选举主节点
当主节点宕机后,需要在slave中选举一个主节点,选举依据:
- 首先会判断slave结点与master结点断开时间长短,如果超过指定值(down-after-milliseconds),直接排除该slave结点(与主节点断开时间过长,缺失数据过多,不适合选举主节点)
- 判断slave节点的slave-priority值(配置文件中配置。默认一样),越小优先级越高,如果为0则不参加选举
- 如果slave-priority一样,则判断slave结点的offset值,越大说明数据越新,优先级越高
- 最后判断运行id(启动顺序),越小优先级越高
故障转移
当主节点(7001)宕机后,选举slave(7002)为主节点后,故障转移的步骤如下:
- sentinel给备选节点发送slaveof no one命令,让该节点成为新的master
- sentinel给其他slave节点发送slaveof 新的主节点ip 新的主节点端口 命令,让其他slave节点成为新的主节点的从节点,开始从新的主节点上同步数据。
- 将原来的主节点标记为slave(实际上是在配置文件中添加了slaveof命令),再重启后自动成为新的主节点的从节点
搭建哨兵集群
我们还是在同一台虚拟机上去搭建哨兵集群。具体命令如下
bash
# 进入/tmp目录
cd /tmp
# 创建目录
mkdir s1 s2 s3
# 添加配置文件
vi s1/sentinel.conf
配置如下信息
bash
port 27001 #端口后面两个设置为27002 27003
sentinel announce-ip 192.168.150.101 # 声明Sentinel的IP地址
sentinel monitor 集群名称(自定义) 192.168.150.101 7001 2 # 监控主节点的地址 2代表指定的数量来决定节点主观下线
sentinel down-after-milliseconds 集群名称 5000 # 指定slave与master断开超过时间失去选举权
sentinel failover-timeout mymaster 60000 # slave故障恢复的时间
dir "/tmp/s1" # 工作目录
接着配置s2、s3目录下的配置文件
bash
# 方式一:逐个拷贝
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
启动
bash
# 第1个
redis-sentinel s1/sentinel.conf
# 第2个
redis-sentinel s2/sentinel.conf
# 第3个
redis-sentinel s3/sentinel.conf
接下来主动断开7001的服务,模拟主节点宕机。观察哨兵集群打印的消息
去查看7003的打印信息
接着查看sentinel信息
观察7002节点信息
重启7001节点,观察主节点信息
RedisTemplate的哨兵模式
将资料中的redis-demo文件使用IDEA打开
在pom文件中引入redis的starter依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在配置文件中application.yml中指定sentinel相关信息
bash
spring:
redis:
sentinel:
master: mymaster #指定集群名称
nodes: # 配置sentinel集群信息
- 192.168.116.131:27001
- 192.168.116.131:27002
- 192.168.116.131:27003
配置读写分离
java
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
return configBuilder ->configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
这里的ReadFrom是配置Redis的读取策略,是一个枚举类,包括如下选择:
- MASTER:从主节点读取
- MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
- REPLICA:从slave节点读取
- REPLICA_PREFERRED:优先从slave节点读取,所有的slave都不可用时才读取master
确保redis集群中存在数据后,启动并访问get/{key}(key为redis中的key名称),并观察控制台
接着执行一次set操作,访问/set/{key}/{value}接口
接下来测试故障转移,将7003宕机,观察sentinel控制台
可以看到又将7001作为主节点了。接下来看到Java客户端又输出很多打印信息
可以看到,Java客户端只需要连接哨兵集群,就可以动态的获取到主节点信息与从节点信息。