Redis持久化(一)——-AOF

AOF日志

AOF (Append Only File) 持久化功能,只会记录写操作命令(因为记录读命令没意义)。Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,来恢复缓存数据。

Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的。这么做的目的在于:

  • 避免额外的检查开销。先将写操作命令记录到 AOF 日志里,再执行命令的话,如果当前的命令语法有问题,那错误的命令就记录到 AOF 日志里了,Redis 在使用日志恢复数据时,就可能会出错。因此先写日志的话需要进行语法检查。
  • 不会阻塞当前写操作命令的执行。因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
    但也存在着问题:
  • 有可能会造成数据丢失。执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
  • 会阻塞下一个命令。下一个命令到达时,如果正在向硬盘写日志,就会阻塞。

这两个问题都与AOF 日志写回硬盘的时机有关,为此,Redis 提供了 3 种写回硬盘的策略:

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

从代码的角度来看,这三种策略只是在控制 fsync() 函数(Redis中用于刷盘)的调用时机:

  • Always 策略就是每次写入 AOF 文件数据后,就执行 fsync() 函数;
  • Everysec 策略就会创建一个异步任务来执行 fsync() 函数;
  • No 策略就是永不执行 fsync() 函数,而是由操作系统内核决定什么时候刷盘。

根据业务场景来进行这三种策略的选择:

  • 如果要高性能,且允许数据丢失,就选择 No 策略;
  • 如果要不允许数据丢失,就选择 Always 策略;
  • 如果允许数据丢失一点,但又想性能高,就选择 Everysec 策略。

注:这与MySQL是不同的,MySQL连接后,有语法检查,通过语法检查后才会执行SQL语句。又因为MySQL的架构分为引擎层和服务层。服务层的binlog也是通过追加的方式记录日志,称之为逻辑日志。引擎层根据选择的存储引擎不同,日志也是不同的,以InnoDB来说,有undo日志和redo日志,undo日志主要用来作数据回滚的,而redo日志支持故障恢复,称之为物理日志,redo日志和binlog配合(涉及到两阶段提交)可以实现持久化。在MySQL中一条更新语句的执行过程大致为:

  1. 客户端先通过连接器建立连接,连接器自会判断用户身份;
  2. 因为这是一条 update 语句,所以不需要经过查询缓存,但是表上有更新语句,是会把整个表的查询缓存清空的,所以说查询缓存很鸡肋,在 MySQL 8.0 就被移除这个功能了;
  3. 解析器会通过词法分析识别出关键字 update,表名等等,构建出语法树,接着还会做语法分析,判断输入的语句是否符合 MySQL 语法;
  4. 预处理器会判断表和字段是否存在;
  5. 优化器确定执行计划,因为 where 条件中的 id 是主键索引,所以决定要使用 id 这个索引;
  6. 执行器负责具体执行,找到这一行,然后更新;
  7. MySQL 会隐式开启事务来执行“增删改”语句,每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里。
  8. 为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了,然后在合适的时机刷盘,也就是WAL(Write-Ahead Logging),MySQL 的写操作并不是立刻写到磁盘上,而是先写日志(redolog),然后在合适的时间再写到磁盘上。
  9. 写redolog是有两个阶段的,记录一条日志的时候是prepare,然后再在binlog里追加日志,之后再将redolog里的日志标记为commit。这么做是为了保证故障恢复时数据一致性,因为MySQL的存储引擎是插入式,InnoDB是后来才作为默认引擎的,也可以被替换,涉及到一些历史原因,已经写了太多MySQL内容就不再展开了,后面我再具体写一篇MySQL的日志总结。

写这个备注的原因是为了说明日志先写还是后写并不是绝对的,而是跟软件的整体设计有关系

AOF重写机制

AOF 日志文件很大时会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。所以,Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。

重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,采用子进程的好处在于:

  • 子进程进行 AOF 重写期间,主进程不会阻塞;
  • 子进程带有主进程的数据副本。

但有个问题,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据会不一致

Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,主进程收到该信号后,会调用一个信号处理函数,来

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件

整个 AOF 后台重写过程中,除了发生写时复制会对主进程造成阻塞,还有信号处理函数执行时也会对主进程造成阻塞,在其他时候,AOF 后台重写都不会阻塞主进程。