侧边栏壁纸
  • 累计撰写 244 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论
隐藏侧边栏

Redis 数据持久化机制(上):AOF 日志篇

kaixindeken
2021-04-28 / 0 评论 / 0 点赞 / 65 阅读 / 3,695 字

数据持久化引入

撑起 Redis 高性能的三大基石分别是基于内存操作、多路复用的非阻塞单线程 IO 模型、以及底层高效的数据结构设计。这里面基于内存操作的功劳不容小觑,而且厥功至伟。

不过任何事物都有其两面性,相较于磁盘操作,内存操作性能确实可以高出好几个数量级,但是缺点也很明显 —— 不能持久化数据,这意味着如果运行 Redis 服务的机器重启,那么所有存储在内存中的数据会面临丢失的风险。如果你只是使用 Redis 作为缓存服务倒还好,就像 Memcached 一样,但是如果你还使用了 Redis 提供其他需要持久化数据的服务,比如消息队列、NoSQL 数据库、或者分布式 Session,那么重启后数据丢失将是不能容忍的。

即便是作为缓存服务,如果系统并发很高,Redis 缓存命中率很高,系统重启会导致大量请求打到数据库,这可能造成数据库无法处理这样规模的并发请求进而导致系统服务不可用,我们将这样的情景称之为缓存雪崩。要解决缓存雪崩问题,可以通过部署 Redis 集群提高系统可用性,避免单点故障。

俗话说「没有金刚钻,别揽瓷器活」,Redis 既然可以用来提供这些需要持久化数据的服务,自然是有万全之策,否则也就没有人敢用了,这也是 Redis 相较于 Memcached 除了支持更加丰富的数据类型外,另一个重要的不同之处 —— 支持持久化数据。

Redis 数据持久化实现

Redis 支持两种数据持久化机制 —— AOF 日志和 RDB 快照。

注:这两种机制都会将内存数据持久化到磁盘中。

AOF 日志是一种增量备份日志,会记录 Redis 服务端所有执行的指令,当系统重启后,可以读取 AOF 日志再次执行所有指令来达到恢复内存数据的目的(这个过程被称作重放),但是如果系统数据量很大,达到 GB 级别,通过 AOF 日志恢复数据可能会耗费数小时之久,对于生产环境而言,这是无法接受的。

为了优化 AOF 日志,Redis 会定期进行 AOF 重写,对 AOF 日志进行瘦身,饶是如此,对于大型系统而言,AOF 日志还是很大,因此,Redis 又提供了 RDB 快照机制。顾名思义,RDB 快照是 Redis 内存数据的一次全量备份,是 Redis 某个时刻所有数据的「快照」,与 AOF 增量日志记录文本格式的 Redis 指令不同,RDB 快照是内存数据的二进制序列化形式,在存储上非常紧凑,因此尺寸比 AOF 日志小很多,通过 RDB 恢复数据也比 AOF 快很多。

不过 RDB 快照也有其缺点,一次快照要备份当前 Redis 系统的所有数据,并且备份期间还要不影响主线程处理客户端请求,所以非常消耗系统资源,如果数据量很大的话,同样会很耗时,因此不可能做到像 AOF 日志那样即时增量备份,这就必然导致 RDB 快照相较于当前最新的 Redis 系统而言,数据存在滞后,这样一来,通过 RDB 快照恢复数据很有可能会出现不是最新数据的情况。

为了充分发挥 AOF 日志和 RDB 快照的优点,同时避免它们的缺点,从 Redis 4.0 开始,采用了一种称之为混合持久化的机制 —— 将 RDB 快照和 AOF 日志文件存在一起,这里的 AOF 日志不再是全量日志,而是两次 RDB 快照期间发生的增量 AOF 日志,通常这部分 AOF 日志很小,然后系统重启后,Redis 服务端先通过 RDB 快照快速恢复内存数据,再通过 AOF 日志恢复滞后部分的增量数据,从而达到恢复全量最新数据的目的。

注:是不是对这个持久化机制似曾相识?没错,MySQL 数据恢复也采用了类似的机制:通过 binlog 以二进制格式记录全量日志,以 redolog 记录事务执行期间已提交未持久化的日志。而在 binlog 的底层实现中,也进一步细分了 ROW、STATEMENT、MIXED 等日志格式。

AOF 日志

了解了 Redis 持久化机制的整体全貌后,接下来,我们来深入探讨 AOF 日志和 RDB 快照的具体实现细节,首先来看 AOF 日志。

写后日志

AOF 的全称是 Append Only File,它是一种增量备份日志,不过与 MySQL 先写日志、再写磁盘的写前日志(Write-Ahead Logging,简称 WAL)相反,AOF 是写后日志,即先执行命令、再写入内存、最后记录到日志。

为什么要这么做呢?因为 Redis 为了提高性能,节省不必要的系统开销,在记录 AOF 日志的时候,并不会检查客户端请求的指令语法是否正确,如果先记日志再执行的话,可能会记录语法错误的指令,从而导致恢复数据时出错。

通过写后日志,指令执行成功后才会记录到 AOF 日志,否则就会给客户端报错,要求传递正确的 Redis 指令,这样就不会导致 AOF 日志记录错误的指令了。另外,这样做的另一个潜在好处是不会阻塞 Redis 写操作。

回写策略

但是这样一来,就会带来我们在 MySQL 中多次提到的隐患 —— 如果 Redis 指令执行成功,在日志还没有记录到 AOF 日志之前 Redis 服务/系统崩溃,重启后就会导致无法恢复最新数据。

另外,记录 AOF 日志也是在 Redis 主线程中进行的,AOF 日志的写入虽然不会阻塞当前指令的写操作,但是会阻塞下一个指令的写操作啊,为了解决这两个问题,我们就要合理控制 AOF 日志写入磁盘的时机。

Redis 为 AOF 日志专门提供了一个配置项 appendfsync,用于配置 AOF 日志写入磁盘的时机:

# appendfsync always
appendfsync everysec
# appendfsync no

我们来看看每个配置值的含义和优缺点:

  • always:同步写入 —— 每个 Redis 写命令执行完,立即将日志写入磁盘,这种策略可以做到数据基本不丢失(数据最安全),但是过于频繁地操作磁盘显然会影响 Redis 主线程性能(性能最差);
  • everysec:每秒写入 —— 每个 Redis 写命令执行完,先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘,这种策略既可以将数据丢失的风险降低(1s内可更新数据能会丢失),又可以避免同步写入磁盘对 Redis 性能的影响,是一种兼顾性能和数据安全的折中方案;
  • no:操作系统控制写入时机 —— 每个 Redis 写命令执行完,先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写入磁盘,这种策略下性能是最好的,但控制权已经不在 Redis 手中了,只要日志没有写入磁盘,则数据全部丢失,完全不可控(数据最不安全)。

因此,在 redis.conf 配置文件中,默认使用的是 everysec,即每秒写入策略,它是一种折中方案,是综合来看最好的选择,建议生产环境使用这个配置值。

AOF 日志写入磁盘操作通过调用操作系统提供的 fsync 函数完成,这是一个耗时的磁盘 IO 操作。

AOF 重写

不管使用那种回写策略,随着系统的运行,AOF 作为增量日志,都会越来越臃肿和庞大,这将为 Redis 服务重启通过 AOF 日志恢复数据带来隐患,为此,Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。

瘦身原理很简单,就是将每个键的多个操作指令合并为一条(以最后更新的值为准)。具体实现是执行 bgrewriteaof 指令时,会 fork 出一个子线程,异步遍历 Redis 数据库所有键,并将其转化为一系列 Redis 操作指令(比如键值对 testkey:testvalue 会生成 set testkey testvalue 指令),存放到一个新的 AOF 日志,存放完成后,再将这个过程中产生的 AOF 增量日志追加到这个新的 AOF 日志,最后再用新的 AOF 日志替代老的,AOF 瘦身工作就完成了。

之所以 fork 一个新的子线程进行 AOF 重写,是为了避免阻塞 Redis 主线程,造成性能下降(Redis 性能优先,处处透露着这种提升性能的思想)。这个子线程在 fork 的时候,会将当前主线程的数据拷贝过来,然后在不影响主线程处理客户端请求的情况下,遍历这些拷贝过来的 Redis 数据副本进行 AOF 重写,这个时候,主线程也会产生新的 AOF 增量日志,当子线程完成数据副本的 AOF 重写后,还要将这些新增的 AOF 日志追加过来,才能形成最新的 AOF 日志。

整个过程如下所示:

1.jpeg

0

评论区