跳转至

🟢 持久化

约 4089 个字 1 张图片 预计阅读时间 20 分钟

AOF 和 RDB?

AOF

每执行一条 写操作 命令,就将该命令以追加的方式写入到 AOF 文件,然后在恢复时,以逐一执行命令的方式来进行数据恢复。用 AOF 日志的方式来恢复数据很慢,因为 Redis 执行命令由单线程负责的,AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个过程就会很慢了。

RDB

RDB 快照是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

RDB 快照是全量快照,每次把内存中所有数据写入磁盘。生成方式有两种:save 在主线程执行会阻塞;bgsave 创建子进程,不阻塞主线程。可通过配置自动触发 bgsave,例如:

  • save 900 1:900 秒内至少 1 次修改
  • save 300 10:300 秒内至少 10 次修改
  • save 60 10000:60 秒内至少 10000 次修改

满足任一条件即执行 bgsave。频率过高影响性能,过低则故障时丢失数据更多,需权衡。

AOF-RDB 混用

在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,加载速度会很快;后半部分 AOF 增量可减少数据丢失。缺点:AOF 可读性变差;且混合持久化后的 AOF 不能在 Redis 4.0 之前版本使用,兼容性差。

AOF 的三种写回策略?

Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上从低到高。

  • Always 每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘
  • Everysec 每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘
  • No 不控制写回硬盘的时机。每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘

AOF 的硬盘重写机制?

随着执行的命令越多,AOF 文件的体积自然也会越来越大,为了避免日志文件过大, Redis 提供了 AOF 重写机制,它会直接扫描数据中所有的键值对数据,然后为每一个键值对生成一条写操作命令,接着将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。

为什么先执行 Redis 命令,再把数据写入 AOF 日志呢?

好处

  • 保证正确写入:如果当前的命令语法有问题,错误的命令记录到 AOF 日志里后可能还会进行语法检查。先执行 Redis 命令,再把数据写入 AOF 日志可以保证写入的都是正确可执行的命令
  • 不阻塞当前写操作:因为当写操作命令执行成功后才会将命令记录到 AOF 日志,避免写入阻塞

缺陷

  • 数据可能会丢失: 执行写操作命令和记录日志是两个过程,Redis 还没来得及将命令写入到硬盘时发生宕机,数据会有丢失的风险
  • 阻塞其他操作: 不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入硬盘的时候,还是会阻塞后续的操作无法执行

AOF 的重写的具体过程?

触发重写机制后,主进程会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读。重写 AOF 子进程读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志。

在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。

AOF 子进程的内存数据跟主进程的内存数据不一致怎么办?

Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会 同时将这个写命令写入到 AOF 缓冲区和 AOF 重写缓冲区。当子进程完成 AOF 重写工作后,会向主进程发送一条信号。主进程收到该信号后,会调用一个信号处理函数,将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

AOF 重写

Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这有两个好处:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
  • 子进程带有主进程的数据副本,使用子进程而不是线程。因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,会发生写时复制,于是父子进程就有了独立的数据副本,不用加锁来保证数据安全

RDB 在执行快照的时候,数据能修改吗?

可以。执行 bgsave 过程中,Redis 依然 可以继续处理操作命令 的,数据是能被修改的,采用的是写时复制技术(Copy-On-Write, COW)。执行 bgsave 命令的时候,会通过 fork()创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,由于共享父进程的所有数据,可以直接读取主线程里的内存数据,并将数据写入到 RDB 文件。此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。如果主线程要修改共享数据里的某一块数据,就会发生写时复制,数据的物理内存就会被复制一份,主线程在这个数据副本进行修改操作。与此同时,子进程可以继续把原来的数据写入到 RDB 文件。

Redis 过期机制?

三种过期删除策略:

  • 定时删除:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器执行 key 的删除操作
    • 优点:内存可以被尽快地释放。定时删除对内存是最友好的
    • 缺点:定时删除策略对 CPU 不友好,删除过期 key 可能会占用相当一部分 CPU 时间,CPU 紧张的情况下将 CPU 用于删除和当前任务无关的过期键上,会对服务器的响应时间和吞吐量造成影响
  • 惰性删除:不主动删除过期键,每次从数据库访问 key 时检测 key 是否过期,如果过期则删除该 key
    • 优点:只会使用很少的系统资源,对 CPU 最友好
    • 缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放。惰性删除策略对内存不友好
  • 定期删除:每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期 key
    • 优点:限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用
    • 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。难以确定删除操作执行的时长和频率

Redis 选择惰性删除+定期删除这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:

  • 如果过期:删除该 key,然后返回 null 客户端
  • 如果没有过期:不做任何处理,然后返回正常的键值对给客户端

从过期字典中随机抽取 20 个 key;检查这 20 个 key 是否过期,并删除已过期的 key;已过期 key 的数量占比随机抽取 key 的数量大于 25%,则继续重复步骤直到比重小于 25%。

Redis 的内存淘汰策略?

不进行数据淘汰的策略

它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,只是单纯的查询或者删除操作的话还是可以正常工作。

进行数据淘汰的策略

在设置了过期时间的数据中进行淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值
  • volatile-ttl:优先淘汰更早过期的键值
  • volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值
  • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值
  • allkeys-lru:淘汰整个键值中最久未使用的键值
  • allkeys-lfu:淘汰整个键值中最少使用的键值

LRU 算法和 LFU 算法有什么区别?

LRU 算法

LRU 全称是 Least Recently Used(最近最少使用),会选择淘汰最近最少使用的数据。

传统 LRU 算法基于链表,最新操作的键会被移动到表头,需要内存淘汰时删除链表尾部的元素。Redis 没有使用这种方式,因为传统 LRU 算法存在两个问题: - 需要用链表管理所有的缓存数据,带来额外的空间开销 - 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时

Redis 如何实现 LRU 算法?

Redis 实现的是**近似 LRU 算法**,在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。

当 Redis 进行内存淘汰时,会使用**随机采样的方式来淘汰数据**,随机取 5 个值(此值可配置),然后**淘汰最久没有使用的那个**。

优点: - 不用为所有的数据维护一个大链表,节省了空间占用 - 不用在每次数据访问时都移动链表项,提升了缓存的性能

缺点:无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。

LFU 算法

LFU 全称是 Least Frequently Used(最近最不常用的),LFU 算法是根据数据访问次数来淘汰数据的,核心思想是"如果数据过去被访问多次,那么将来被访问的频率也更高"。

LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。

Redis 如何实现 LFU 算法?

LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同:

  • 在 LRU 算法中:Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,Redis 可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key
  • 在 LFU 算法中:Redis 对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),用来记录 key 的访问时间戳;低 8bit 存储 logc(Logistic Counter),用来记录 key 的访问频次

Redis 持久化时对过期键会如何处理的?

RDB

RDB 分文生成阶段和加载阶段,生成阶段会对 key 进行过期检查,过期的 key 不会保存到 RDB 文件中;加载阶段看服务器是主服务器还是从服务器,如果是主服务器,在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中;如果从服务器,在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。过期键对载入 RDB 文件的从服务器也不会造成影响。

AOF

AOF 文件写入阶段和 AOF 重写阶段。写入阶段如果数据库某个过期键还没被删除,AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。重写阶段会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中。

Redis 主从模式中,对过期键会如何处理?

从库不会进行过期扫描,即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值。从库的过期键处理依靠主服务器控制, 主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

评论