Redis 分布式缓存

Redis 分布式缓存

基于 Redis 集群解决单机 Redis 存在的问题,单机的 Redis 存在四大问题:

image-20210725144240631

1. 持久化

1.1 RDB 持久化

RDB 全称 Redis Database Backup file(Redis 数据备份文件),也被叫做 Redis 数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当 Redis 实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为 RDB 文件,默认是保存在当前运行目录

1.1.1 执行时机

RDB 持久化在四种情况下会执行:

  1. 执行 save 命令
  2. 执行 bgsave 命令
  3. Redis 停机时
  4. 触发 RDB 条件时

1)save 命令

执行下面的命令,可以立即执行一次 RDB:

image-20210725144536958

save 命令会导致主进程执行 RDB,这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到

2)bgsave 命令

下面的命令可以异步执行 RDB:

image-20210725144725943

这个命令执行后会开启独立进程完成 RDB,主进程可以持续处理用户请求,不受影响

3)停机时

Redis 停机时会执行一次 save 命令,实现 RDB 持久化

4)触发 RDB 条件

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

redis6.0.16 以下默认情况:

# 900秒内,如果至少有1个key被修改,则执行bgsave,如果是save "" 则表示禁用RDB
save 900 1  
save 300 10  
save 60 10000 

redis6.2 和 redis7.0.0 默认情况:

save 3600 1
save 300 100
save 60 10000

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

# 是否压缩,建议不开启,压缩也会消耗cpu,磁盘的话不值钱
rdbcompression yes

# RDB文件名称
dbfilename dump.rdb  

# 文件保存的路径目录
dir ./ 

1.1.2 RDB 原理

bgsave 开始时会 fork 主进程得到子进程,子进程共享主进程的内存数据。完成 fork 后读取共享内存数据并写入 RDB 文件替换旧的 RDB 文件

当子进程在访问共享内存数据写 RDB 文件时,主进程若要执行写操作,采用的是 copy-on-write 技术

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,将共享数据设置为 read-only,拷贝一份数据,执行操作,之后的也会在副本上进行

image-20210725151319695

1.1.3 小结

RDB 方式 bgsave 的基本流程?

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

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

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

RDB 的缺点?

  • RDB 执行间隔时间长,两次 RDB 之间写入数据有丢失的风险
  • fork 子进程、压缩、写出 RDB 文件都比较耗时

1.2 AOF 持久化

AOF 全称为 Append Only File(追加文件)。Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件

image-20210725151543640

1.2.1 AOF 配置

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

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

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

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always 
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec 
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no

三种策略对比:

image-20210725151654046

1.2.2 AOF 文件重写

因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。通过执行 bgrewriteaof 命令,可以让 AOF 文件执行重写功能,用最少的命令达到相同效果

image-20210725151729118

如图,AOF 原本有三个命令,但是 set num 123 和 set num 666 都是对 num 的操作,第二次会覆盖第一次的值,因此第一个命令记录下来没有意义,所以重写命令后,AOF 文件内容就是:mset name jack num 666

Redis 也会在触发阈值时自动去重写 AOF 文件。阈值也可以在 redis.conf 中配置:

# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写 
auto-aof-rewrite-min-size 64mb 

满足 64M 的时候,满足 percentage 才会触发重写,文件太小重写没意义

1.2.3 AOF 文件格式

AOF 文件保存位置:

redis6:和 RDB 保存的位置相同

redis7:会在 RAB 文件保存的位置上加上一个自己设定的 appendonlydir 目录,保存在其中

文件格式:

redis6:apendfilename.aof

redis7:aof 文件被拆分为三个

image-20231013214118098

image-20231013214141914

1.3 RDB 与 AOF 对比

RDB 和 AOF 各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用

image-20210725151940515

1.4 RDB-AOF 混合持久化

重启 Redis 时,我们很少使用 RDB 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。 Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。通过如下配置可以开启混合持久化(必须先开启 AOF)

 # aof‐use‐rdb‐preamble yes

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为 RESP 命令写入 AOF 文件,而是将重写这一刻之前的内存做 RDB 快照处理,并且将 RDB 快照内容和增量的 AOF 修改内存数据的命令存在一起,都写入新的 AOF 文件,新的文件一开始不叫appendonly.aof,等到重写完新的 AOF 文件才会进行改名,覆盖原有的 AOF 文件,完成新旧两个 AOF 文件的替换。于是在 Redis 重启的时候,可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅得到提升

AOF 文件再经过 bgrewriteaof 之后,再次执行的命令会按照 AOF 的方式进行存储,等下次再 bgrewriteaof 之后,再进行二进制存储。redis 后台再进行 bgrewriteaof 的时候,这时候如果有客户端命令进来,会再后面进行追加

image.png

Redis4.0 之后新增了混合持久化存储方案,实际就是利用 AOF 的方式加上 RDB 二进制数据存储方式的合并,建议在实际生成过程中可以使用混合持久化方案,此时可以将 RDB 关掉,只需要在redis.conf中的 save 注释即可

2. 主从

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

image-20210725152037611

2.1 主从同步原理

2.1.1 全量同步

主从第一次建立连接时,会执行全量同步,将 master 节点的所有数据都拷贝给 slave 节点,流程:

image-20210725152222497

这里有一个问题,master 如何得知 salve 是第一次来连接呢

有几个概念,可以作为判断依据:

  • 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 才可以判断到底需要同步哪些数据

因为 slave 原本也是一个 master,有自己的 replid 和 offset,当第一次变成 slave,与 master 建立连接时,发送的 replid 和 offset 是自己的 replid 和 offset

master 判断发现 slave 发送来的 replid 与自己的不一致,说明这是一个全新的 slave,就知道要做全量同步

master 会将自己的 replid 和 offset 都发送给这个 slave,slave 保存这些信息。以后 slave 的 replid 就与 master 一致了

因此,master 判断一个节点是否是第一次同步的依据,就是看 replid 是否一致

如图:

image-20210725152700914

完整流程描述:

  • slave 节点请求增量同步
  • master 节点判断 replid,发现不一致,拒绝增量同步
  • master 将完整内存数据生成 RDB,发送 RDB 到 slave
  • slave 清空本地数据,加载 master 的 RDB
  • master 将 RDB 期间的命令记录在 repl_baklog,并持续将 log 中的命令发送给 slave
  • slave 执行接收到的命令,保持与 master 之间的同步

2.1.2 增量同步

全量同步需要先做 RDB,然后将 RDB 文件通过网络传输个 slave,成本太高了。因此除了第一次做全量同步,其它大多数时候 slave 与 master 都是做增量同步

什么是增量同步?就是只更新 slave 与 master 存在差异的部分数据。如图:

image-20210725153201086

那么 master 怎么知道 slave 与自己的数据差异在哪里呢?

2.1.3 repl_baklog 原理

master 怎么知道 slave 与自己的数据差异在哪里呢?

这就要说到全量同步时的 repl_baklog 文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从 0 开始读写,这样数组头部的数据就会被覆盖。repl_baklog 中会记录 Redis 处理过的命令日志及 offset,包括 master 当前的 offset,和 slave 已经拷贝到的 offset:

image-20210725153359022

slave 与 master 的 offset 之间的差异,就是 salve 需要增量拷贝的数据了。

随着不断有数据写入,master 的 offset 逐渐变大,slave 也不断的拷贝,追赶 master 的 offset:

image-20210725153524190

直到数组被填满:

image-20210725153715910

此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到 slave 的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。但是,如果 slave 出现网络阻塞,导致 master 的 offset 远远超过了 slave 的 offset:

image-20210725153937031

如果 master 继续写入新数据,其 offset 就会覆盖旧的数据,直到将 slave 现在的 offset 也覆盖:

image-20210725154155984

棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果 slave 恢复,需要同步,却发现自己的 offset 都没有了,无法完成增量同步了只能做全量同步

image-20210725154216392

2.2 主从同步优化

主从同步可以保证主从数据的一致性,非常重要

因为全量同步非常消耗性能,repl_baklog 写满时还会发生全量同步,为了减少或优化全量同步

可以从以下几个方面来优化 Redis 主从集群

  1. 在 master 中配置 repl-diskless-sync yes 启用 无磁盘复制,直接将生成的 RDB 发送给 salve,避免全量同步时的磁盘 IO,提高全量同步性能。(磁盘 IO 能力较差,网络强时)
  2. 设置 Redis 单节点上的内存占用不要太大,减少 RDB 导致的过多磁盘 IO
  3. 适当提高 repl_baklog_buffer 的大小,参数为 repl-baklog-size(默认为 1MB)发现 slave 宕机时尽快实现故障恢复,尽可能避免全量同步
  4. 限制一个 master 上的 slave 节点数量,如果实在是太多 slave,则可以采用主 - 从 - 从链式结构,减少 master 压力

主从从架构图:

image-20210725154405899

2.3 小结

简述全量同步和增量同步区别?

  • 全量同步:master 将完整内存数据生成 RDB,发送 RDB 到 slave。后续命令则记录在 repl_baklog,逐个发送给 slave
  • 增量同步:slave 提交自己的 offset 到 master,master 获取 repl_backlog 中从 offset 之后的命令给 slave

什么时候执行全量同步?

  • slave 节点第一次连接 master 节点时
  • slave 节点断开时间太久,repl_baklog 中的 offset 已经被覆盖时

什么时候执行增量同步?

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

3. 哨兵

Redis 提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复

哨兵的结构如图:

image-20210725154528072

哨兵的作用如下:

  • 监控:Sentinel 会不断检查 master 和 slave 是否按预期工作
  • 自动故障恢复:如果 master 故障,Sentinel 会将一个 slave 提升为 master。当故障实例恢复后也以新的 slave 为主
  • 通知:Sentinel 充当 Redis 客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给 Redis 的客户端

3.1 监控原理

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

  • 主观下线:如果某 sentinel 节点发现某实例未在规定时间响应,则认为该实例主观下线
  • 客观下线:若超过指定数量(quorum)的 sentinel 都认为该实例主观下线,则该实例客观下线。quorum 值最好超过 Sentinel 实例数量的一半

image-20210725154632354

3.2 故障恢复原理

一旦发现 master 故障,会在 sentinel 集群中选出一个 sentinel-leader,采用 Raft 算法

  • Raft 算法的基本思路是先到先得
  • 比如 sentinel 1 首先发现 master 宕机,那么他就会向 sentinel 2 和 3 发送请求成为 leader,若 sentinel 2 和 3 没有接受 / 同意过别人的请求,就会同意 sentinel 1 的请求

一旦发现 master 故障,sentinel-leader 需要在 salve 中选择一个作为新的 master,选择依据是这样的:

  1. 首先会判断 slave 节点与 master 节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该 slave 节点
  2. 然后判断 slave 节点的 slave-priority 值,越小优先级越高,如果是 0 则永不参与选举
  3. 如果 slave-prority 一样,则判断 slave 节点的 offset 值,越大说明数据越新,优先级越高
  4. 最后是判断 slave 节点的运行 id 大小,越小优先级越高

当选出一个新的 master 后,该如何实现切换呢?

流程如下:

  1. sentinel 给备选的 slave1 节点发送 slaveof no one 命令,让该节点成为 master
  2. sentinel 给所有其它 slave 发送 slaveof 192.168.111.100 7002 命令,让这些 slave 成为新 master 的从节点,开始从新的 master 上同步数据
  3. 最后,sentinel 将故障节点标记为 slave,当故障节点恢复后会自动成为新的 master 的 slave 节点

image-20210725154816841

3.3 小结

Sentinel 的三个作用是什么?

  • 监控
  • 故障转移
  • 通知

Sentinel 如何判断一个 redis 实例是否健康?

  • 每隔 1 秒发送一次 ping 命令,如果超过一定时间没有相向则认为是主观下线
  • 如果大多数 sentinel 都认为实例主观下线,则判定服务下线

故障转移步骤有哪些?

  • 首先选定一个 slave 作为新的 master,执行 slaveof no one
  • 然后让所有节点都执行 slaveof 新 master
  • 修改故障节点配置,添加 slaveof 新 master

4. 分片集群

4.1 分片集群的特征

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

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

使用分片集群可以解决上述问题,如图:

image-20210725155747294

分片集群特征:

  • 集群中有多个 master,每个 master 保存不同数据
  • 每个 master 都可以有多个 slave 节点
  • master 之间通过 ping 监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

4.2 散列插槽 slot

4.2.1 插槽原理

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

image-20210725155820320

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

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

为什么是 16384(2^14)个槽,CRC16 算法产生的 hash 值有 16bit,为什么不使用 2^16=65536 个槽位呢?

image-20231016230439953

(1)65536 个 slot 槽 ping 心跳包的消息头太大

image-20230410223821799

(2)Redis 作者推荐集群 1000 以内节点,16384 个槽够用了,因为集群节点数太多 ping 心跳包也会很大,消耗带宽,容易造成网络拥堵

image-20230410223913265

(3)在节点少的情况下,即小型集群中,因为填充率为 slots/N,若采用 65536 的话,填充率将会很高,压缩比将会很低,不容易传输,但是采用 16384 的话,填充率低一些,压缩比将会高很多,容易传输些

4.2.2 槽位映射算法:一致性哈希算法

要将数据缓存在集群的哪一台节点上,采用哈希算法,这里有两种哈希算法:

  1. 哈希取余算法:计算公式:hash (key) % N ,N 为集群中节点的数量

    • 这种哈希算法虽然简单,但是会有一个缺点:当集群数量发生改变的时候,那么计算公式的分母发生改变,之前存储的所有 slot 中的数据,都要重新进行排列
  2. 一致性哈希算法:计算公式:hash (key) % 2^32

    • 目的:当服务器个数发生变动时尽量减少影响客户端到服务器的映射关系

    • 实现:采用一致性哈希环,即令 2^32 = 0 ,让哈希表首位相连成一个环,集群中每个节点落在环上的位置是固定的

      image-20231027232610828

    • 落键规则:hash (key) 并对 2^32 取模过后,将会落在一致性哈希环的某个位置,然后顺时针寻找第一个 Redis 节点,那么此 key 就属于该节点存储

    • 优点:若节点数量发生变化,影响的映射关系也只有出现变化的那个节点逆时针到第一个节点之间的映射关系
      例如:新增了一个节点,那么该节点逆时针到上一个节点之间的数据归新节点所有;删除了一个节点,那么该节点逆时针到上一个节点之间的数据归顺时针下一个节点所有

    • 缺点:当节点太少时,容易发生数据倾斜问题

      image-20231027233202077

4.2.3 客户端操作

连接客户端集群模式:

redis-cli -p 7001 -c
  • -c:集群模式,若不加 - c,根据 key 映射的 slot 不在此节点时将会报错

例如:key 是 num,那么就根据 num 计算,如果是 {itcast} num,则根据 itcast 计算。计算方式是利用 CRC16 算法得到一个 hash 值,然后对 16384 取余,得到的结果就是 slot 值

image-20210725155850200

如图,在 7001 这个节点执行 set a 1 时,对 a 做 hash 运算,对 16384 取余,得到的结果是 15495,因此要存储到 7003 节点。到了 7003 后,执行 get num 时,对 num 做 hash 运算,对 16384 取余,得到的结果是 2765,因此需要切换到 7001 节点

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

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

4.2.4 小结

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

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

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

  • 这一类数据使用相同的有效部分,例如 key 都以 {typeId} 为前缀
🌟 如果您喜欢我的文章,欢迎赞赏支持,您的支持是我创作的最大动力!🌟
🖋 作者:Enndfp
🔗链接:https://blog.enndfp.cn
📜版权声明:您可以自由转载,但请务必注明原文地址,感谢您的尊重与支持~
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇