跳转到内容
唯一赫兹
返回

Redis 持久化

持久化

AOF

AOF (Append Only File)

Redis 默认不开启 AOF,通过修改 redis.conf 启用:

// redis.conf
appendonly        yes                // 是否开启AOF
appendfilename    "appendonly.aof"   // 持久化文件的名称

set name v1hz 为例,AOF 日志内容类似于下面:

*3
$3
set
$4
name
$4
v1hz

*3 表示有三个部分,$3 表示该部分有 3 个字节 Redis 会先执行命令再记录日志,因为

  1. 避免额外的检查开销
  2. 不阻塞当前写操作的执行

AOF 的写回策略

redis.conf 中提供了 appendfsync 来配置写入磁盘的时机:

AOF 重写

如果 AOF 文件过大会导致恢复过程很慢,redis 采用 AOF 重写机制来压缩 AOF 文件

重写的效果

如果执行了 set a 1set a 2,重写后日志中只会记录 set a 2,只记录最终形态,大大压缩了 AOF 的大小

AOF 后台重写

关于子进程

在一个进程内调用 fork 会创建一个子进程,和主进程共享内存(通过只复制目标内存的页表实现),这块内存会被标记为只读,当其中一个进程要修改数据时,会触发「写时复制」,将需要修改的目标内存页复制一份(只会复制要修改的内存页,其他内存依旧可以共享) 如果用线程而不是子进程,为了避免数据同时被多方修改,就要加锁,性能会严重下降,实现复杂度也会上升

RDB

RDB 的使用

Redis 提供了两个命令来生成 RDB 文件:

save 900 1
save 300 10
save 60 10000

save m n 表示在 m 秒内对数据库修改了至少 n 次就会进行一次 bgsave,满足其中一个就会执行

RDB 也使用写时复制
  • 和 AOF 后台重写一样,RDB 也是 fork 出子进程并利用写时复制,系统恰好在 RDB 文件生成后崩溃,则会丢失主进程在快照期间修改的数据
  • 极端情况下,如果快照期间,所有数据都被修改,内存占用会使原先的两倍(所有共享内存都被复制)

AOF+RDB:混合持久化

Redis 4.0 提出混合使用 AOF 和 RDB,也叫混合持久化 Redis 配置文件中开启混合持久化:

aof-use-rdb-preamble yes
为什么用 RDB 格式写入 AOF 文件?

RDB 是紧凑的二进制数据,AOF 是命令日志, 所以本质上使用 RDB 优化了 AOF 的「全量数据表示」部分

大 key 对持久化的影响

过期机制

过期删除策略

如何判断一个 key 是否过期

Redis 内部存储着一个过期字典,包含一个指向键的指针和一个过期时间戳,查询一个 key 时,Redis 会先判断这个 key 是否在过期字典中:

  • 如果不存在,则正常返回值
  • 如果存在,则获取该 key 的过期时间,与系统时间比对来判断过期

Redis 实际选择「惰性删除+定期删除」配合使用:

内存淘汰策略

如果 Redis 的运行内存超过设定的最大内存,会使用内存淘汰策略删除符合条件的 key

Redis 最大允许内存

最大内存通过 maxmemory <bytes> 配置,64 位系统中默认为 0,表示没有限制,32 位系统中默认为 3GB

可以通过 config set/get maxmemory-policy 配置和查看内存淘汰策略 noeviction:不进行内存淘汰,如果运行内存达到设置的最大内存时,如果有新的数据写入会报错通知禁止写入

对设置了过期时间的数据淘汰

volatile-random:随机淘汰设置了过期时间的数据 volatile-ttl:淘汰采样中更早过期的数据 volatile-lru:设置了过期时间的数据中,淘汰采样中最久未被使用的数据 volatile-lfu:设置了过期时间的数据中,淘汰采样中最少被使用的数据

对所有范围的数据淘汰

allkeys-random:随机淘汰 allkeys-lru:淘汰采样中最久未被使用的数据 allkeys-lfu:淘汰采样中最少被使用的数据

Redis 的采样淘汰

Redis 不全局扫描所有的 key,而是随机采样多个 key(默认为 5 个,通过配置调整),从中找出需要淘汰的目标数据

LRU 和 LFU

LRU 和 LFU 的实现方式主要依赖于 Redis 对象头中的一个 24bit 结构,如下:

LRU

正常的 LRU 是通过链表实现的,但是 Redis 为了避免引入额外的内存开销,没有选择使用链表,而是利用对象头中的 24bit,存储该数据的最后一次访问的时间戳

这样的实现有以下优点:

LRU 无法解决缓存污染问题,如果一次读取了大量的无用数据,这些数据只会读取一次却会在 Redis 中留存很长时间,挤占真正的热点数据的空间,LFU 则能提高热数据的缓存命中率

LFU

对象头中的 24bit 分两段来存存储,包括:

  1. 按照上一次访问距离当前的时长,对 logc 进行衰减
  2. 按照一定的概率增加 logclogc 越大增加的概率越低 可以在配置文件通过 lfu-decay-timelfu-log-factor 配置 logc 的衰减和增长速度
lfu-decay-timelfu-log-factor

lfu-decay-time = 10 表示 logc 每 10 分钟减 1 logc 增加的概率 PP 的计算方式为 p = 1 / (logc * lfu_log_factor + 1) 这样的设计有以下几点好处:

  1. logc 是缓慢增加的,8bit (最大 255)也能表示被访问上千次的数据的频次
  2. 根据上一次访问的时间进行衰减,自动淘汰「历史热数据」

分布式锁

分布式锁的多种问题

原子加锁 —— NX/EX

SET lock_key unique_value NX EX 30

NX:表示只当锁不存在时才加锁 EX:表示 30s 后自动过期 unique_value:识别业务方的唯一 id

锁续期 —— 看门狗机制

高可用锁 —— RedLock 算法

分布式锁落地

普通任务 => 单节点锁 + 合理的过期时间 长任务 => 加看门狗实现锁续期 核心业务 => RedLock 实现高可用

高可用

主从复制

Redis 版本之间的命令不同

Redis6.0+ 将 SLAVEOF 命名为 REPLICAOF,二者等效

Redisd 主从复制和 MySQL 主从复制一样,添加多个服务器作为主服务器的副本,所有写操作都在主服务器上进行,将最新的数据同步给从服务器,做到主从数据一致,主、从服务器都能处理读操作,做到读写分离

第一次同步 —— 全量复制

第一阶段 —— 准备全量复制

每个 Redis 服务器启动后会生成一个随机的 runID 来唯一标识自己

  1. 从服务器执行 replicaof <ip> <port> 后,主从服务器建立起 TCP 连接, 然后 Redis 会向主服务器发送 psync <runID> <offset>, 由于是第一次同步不知道主服务器的 runID 所以为 ?, 同步还未开始所以 offset 是 -1
  2. 主服务器接受到从服务器的请求后,返回 FULLRESYNC <runID> <offset>FULLRESYNC 表示主服务器要进行一次全量复制,将所有数据同步给从服务器
  3. 从服务器接收到响应后,保存主服务器的 runID 和同步进度 offset
第二阶段 —— 同步快照数据
  1. 主服务器执行 bgsave 生成 RDB 快照文件,并将文件发送给从服务器
  2. 从服务器接收到文件后,会清空当前数据,然后载入 RDB 文件
  3. 从服务器载入完成后,会返回一个确认响应给主服务器
第三阶段 —— 同步最新数据

主服务器会在以下时间间隙将新的写命令写入 replication buffer 缓冲区中:

  1. 主服务器生成 RDB 文件期间
  2. 主服务器发送 RDB 文件期间
  3. 从服务器载入 RDB 文件期间 主服务器接收到从服务器载入完成的确认消息后,就会将缓冲区中的新命令发送给从服务器,从服务器执行所有的新命令后,主从服务器数据就一致了,第一次同步完成

后续的同步 —— 命令传播

第一次同步完成后,TCP 连接会一直维护着,后续主服务器会通过这个连接继续将新的写命令同步给从服务器,这是 基于长连接的命令传播,避免频繁的 TCP 连接断开和建立带来的性能损耗

网络断开重连后的同步 —— 增量复制

如果网络连接断开,命令传播就无法继续,网络连接恢复后,主从服务器数据不一致 Redis2.8 之前,网络连接恢复后,主从服务器会重写进行一次全量复制 Redis2.8 之后,采用 增量复制 的方式继续同步 增量复制的实现主要依赖以下几个数据:

  1. repl_backlog_buffer:一个环形的缓冲区,主服务器在命令传播时,不仅将命令发送到从服务器,还会写入这个缓冲区
  2. replication_offsetmaster_repl_offset 用于记录主服务器到哪里,slave_repl_offset 用于记录从服务器到哪里
  3. repl_backlog_start_offset:用于记录主服务器中环形缓冲区的起始命令的偏移量

当网络恢复后,从服务器发来 psync <runID> <slave_repl_offset> 主服务器计算 slave_repl_offset >= repl_backlog_start_offset

repl_backlog_buffer 缓冲区大小的设置

缓冲区大小可以根据公式 repl_backlog_buffer_size = seconds * write_size_per_second 计算 seconds 表示从服务器断线后重连上主服务器的预估秒数 write_size_per_second 表示主服务器每秒产生的写命令数据大小 具体的大小可以设置为以此为基础的两倍,用于预防特殊情况

分摊主服务器压力

一个服务器执行 replicaof <ip> <port>,如果目标 IP 的服务器是从服务器,目标服务器会成为「中间主服务器」,当前服务器会成为「下一级从服务器」 这样做有以下几个好处:

  1. 减少主服务器 bgsave 的次数(Redis 主从复制不会复用同一份 RDB 文件)
  2. 节省主服务器的网路带宽

面试题

如何判断 Redis 某个节点是否正常工作?

Redis 通过互相的 ping-pong 心态检测机制来检查节点是否正常工作,如果一半以上的节点 ping 一个节点没有 pong 响应,集群就会认为这个节点挂了,断开和这个节点的连接

主从复制架构中,过期 key 如何处理?

某个 key 被删除有多个时机:

  1. 显示执行 del 命令
  2. 到期被定期/惰性删除
  3. 内存满了,淘汰算法淘汰 主节点会模拟一个 del 命令发送到从节点
Redis 是同步复制还是异步复制?

Redis 接收到写命令后,同步写入内部缓冲区,然后异步发送给从节点

主从复制中的 replication_bufferrepl_backlog_buffer 的区别?
  1. 一个主节点只分配一个 replication backlog buffer;主节点会给每个从节点分配一个 replication buffer
  2. replication backlog buffer 是一个环形缓冲区,满了后会覆盖旧数据;replication buffer 满了会断开连接,删除缓存,从节点重新连接并重新进行全量复制
如何应对主从数据不一致?

主从数据不一致主要是因为,主节点的命令传播是异步进行的,主节点不会等待从节点执行完命令再返回给客户端,所以可能主节点已经返回,而从节点还未执行完成命令。应对方法如下:

  1. 尽量保证主从节点之间的网络连接良好,避免主从节点在不同机房
  2. 开发一个监控程序,当主从节点之间的同步进度差值大于预设阈值,就阻止客户端和读取目标从节点的数据。为了避免客户端和所有从节点都不能连接,要把阈值设置的大一些
主从切换如何减少数据丢失?

主从切换时有两种原因导致数据丢失:

  1. 异步复制断开:主节点的数据还未同步给从节点就宕机,未同步的数据丢失
  2. 集群脑裂:主节点网络出错,和从节点断联,但依旧能和客户端连接,哨兵选出一个新的主节点,网络恢复后出现旧的主节点被降级为从节点,全量复制前清除数据,导致数据丢失

解决方案:

  1. 通过 min-slaves-to-write n 保证主节点至少要有 n 个从节点连接,才允许写操作
  2. 通过 min-slaves-max-lag m 保证主从复制的延迟至少要低于 m 秒,才允许写操作

当客户端发现主节点不可写后,可以先将数据写入本地缓存,或者将数据写入 kafka 消息队列,恢复正常后再重新写入主节点

哨兵(高可用)

哨兵在监控主节点并触发故障转移,先后会有两次投票:

  1. 判断主节点的投票
  2. 选举 leader 的投票

「主观下线」和「客观下线」

为什么客观下线只适用于主节点?

主节点下线会触发故障转移,为避免误判必须让多个哨兵共同确认 从节点故障影响小,无需多哨兵确认

哨兵中的 leader 竞选

主节点故障后,只能由一个哨兵来进行故障转移,所以需要选举出一个哨兵作为 leader

  1. 率先将主节点标记为「客观下线」的哨兵,成为「候选者」
  2. 「候选者」会向其他哨兵发起命令(第二次投票),要求进行投票选取 leader 执行故障转移
  3. 赞成票 超过半数哨兵,并且超过 quorum,「候选者」就能成为 leader
多个候选者的情况、quorum 的设置、哨兵的数量
  • 如果两个哨兵同时将主节点标记为「客观下线」就会出现多个候选者,这时谁的命令先到达其他哨兵,就会先获得赞成票,后到的命令会被拒绝投票
  • quorum 应该设置为 sentinel_nums / 2 + 1
  • 哨兵个数应该大于等于 3 且为奇数,既能容忍部分哨兵故障,还能避免平票

故障转移的过程

一、选出一个新主节点
  1. 过滤掉已下线或网络不稳定的节点
  2. 依次判断:
    1. 优先级:slave-priority 值越小越优先(根据服务器性能手动设置的优先级)
    2. 复制进度:slave_repl_offset 越接近 master_repl_offset 越好
    3. 前两个都一样则选取 id 更小的
  3. 向目标从节点发送 SLAVEOF no one,将其升为新主节点

发送 SLAVEOF no one 命令后,leader 哨兵会每秒向新主节点发送 INFO 命令查看其节点信息(故障转移之前是 10 秒一次) 当节点的角色信息从 slave 变为 master 后,leader 哨兵就知道目标节点已经升级为了新主节点

二、让从节点复制新主节点

leader 哨兵会向其他所有从节点发送 SLAVEOF <new_master_ip> <new_master_port> 命令,使他们全部转为复制新主节点

三、客户端更新连接地址

客户端和哨兵建立连接后,会订阅哨兵提供的频道,从指定频道中获取新的 IP 地址和端口号,自动更新连接地址 哨兵提供的消息订阅频道,常见的如下:

四、将旧主节点切换为从节点

当旧主节点重新上线后,哨兵集群会向其发送 SLAVEOF 命令,使其变为从节点

哨兵集群的组成方式

哨兵节点通过订阅主节点上的频道交换彼此的信息,从而实现互相发现 当添加哨兵时,只需要配置主节点名、主节点 IP 地址和端口、quorum 值:

sentinel monitor <master_name> <master_ip> <master_port> <quorum>

Cluster 集群(高可用 + 高扩展)

Cluster 集群模式中内置高可用,没有哨兵这种角色 下文的「节点」一般指「由多个节点组成的主从结构的分片」 Cluster 集群主要解决以下几个问题:

数据到分片的映射方案 —— 哈希槽

Redis Cluster 采用哈希槽来将数据映射到目标分片

为什么不直接对分片数取模映射到目标分片?
  • Cluster 集群的目标是高可扩展,后续必然会增加/减少分片
  • 模数会变化会导致几乎所有分片中的数据都需要频繁迁移,性能损耗大
  • 哈希槽将模数固定为 16384,增减分片时只需更新分片的槽位信息并迁移部分数据即可

读取时,客户端通过本地槽位信息,或者通过分片返回的重定向信息确认目标数据所在槽位

读取数据的智能寻址 —— MOVED/ASK 重定向

一个节点对请求的处理流程如下:

  1. 计算哈希槽映射,如果目标 key 应在的哈希槽不是自己负责的,则返回 MOVED 重定向;如果自己负责目标 key 应在的哈希槽,则判断该哈希槽是否正在迁出或导入
  2. 如果正在迁出,若 key 存在则返回;不存在则返回 ASK 重定向 —— 客户端接收到 ASK 重定向后,会对重定向的目标节点发送 ASKING 命令,为其添加标记
  3. 如果正在导入,检查是否带有 ASKING 标记,如果有则查找 key 并返回结果;否则返回 MOVED 重定向 —— 客户端收到 MOVED 重定向后,会更新本地槽位信息
  4. 如果哈希槽无迁出/导入状态,则直接查找 key

各节点之间的通信机制 —— Gossip

每个节点周期性地从节点列表中选择 k 个节点,将本节点存储的信息传播出去,直到集群的信息一致 Gossip 通过 集群总线 进行通信,即每个节点都会多开一个端口,端口号为 对外服务端口号 + 10000,通信采用特殊的二进制协议 传播的消息有多种:

集群内的故障发现和转移

故障发现

  1. 资格检查:筛选出正常状态的、和主节点保持较新复制的从节点
  2. 发起选举:符合资格的从节点在一段延迟后(基于复制偏移量,数据越新延迟时间越短),向其他所有主节点请求投票
  3. 晋升:获取超过半数投票的节点胜出,执行 SLAVEOF no one,升为主节点
  4. 集群同步:新主节点通过 Gossip 广播自己的新角色信息
投票中的延迟机制

数据越新延迟时间越短,这奖励了数据更新的节点,同时间接奖励了网络更好的节点,使得更优秀的节点更容易获得投票

缓存

缓存雪崩

主要是由于大量数据同时过期或者 Redis 故障宕机,导致大量请求直接访问数据库,如果数据库撑不住就会导致连锁反应,出现「雪崩」

缓存击穿

某个热点数据过期,也会导致大量请求达到数据库,可以采用与缓存雪崩类似的解决方案

缓存穿透

如果要访问的数据不存在,这个数据即不在缓存也不在数据库,请求会一直访问数据库,导致数据库压力骤增,发生这种情况一般有两种可能:业务误删除恶意攻击

布隆过滤器

布隆过滤器由「一个位图」和「N 个哈希函数」组成

缓存一致性

旁路缓存策略(Cache-Aside)

为什么不先删除缓存后更新数据库

相较于删除缓存,更新数据库更耗时,更有可能出现并发问题! 先更新数据库后删缓存也会有并发问题,只是概率小很多

确保更新和删除都成功

如果删除缓存失败,也会导致数据不一致,解决的关键是异步操作缓存+失败重试

常见面试题

数据结构

常见的数据结构是怎么实现的?

Redis 7.0 之后的变化

Redis7.0 之后,压缩列表被弃用,全部使用 listpack 代替

Redis 线程模型

Redis 是单线程吗?

Redis 的「接收客户端请求 → 解析请求 → 执行操作 → 返回数据给客户端」是由一个线程(主线程)完成的,但是 Redis 程序不是单线程的,Redis 在启动时会创建后台程序:



上一篇
Kafka 入门
下一篇
Java 并发