Redis 每执行一条写操作命令,就把该命令追加写入到一个文件,重启 Redis 时,先去读取这个文件中的命令,并且执行,就恢复了数据(注意只会记录写操作命令,读操作命令不会被记录的,因为没意义)
这种保存写操作命令到日志的持久化方式就是 AOF(Append Only File)
Redis 的 AOF 持久化功能默认是不开启的,需要进行配置:
// redis.conf
appendonly yes // 开启 AOF 持久化(默认为 no)
appendfilename "appendonly.aof" // AOF 持久化写入的文件名
AOF 日志文件就是普通的文本,文本内容有一定的规则:
*3
:当前命令有三个部分$num
:表示这部分有num
个字节
执行写操作命令后,才将该命令记录到 AOF 日志中,这么做有 2 个好处:
- 避免额外的检查开销:执行命令成功后再记录日志,保证记录在 AOF 日志里的命令都是可执行并且正确的
- 不阻塞当前写操作命令的执行
AOF 持久化功能也有一些潜在风险:
- 数据就会有丢失的风险:执行写操作命令和记录日志是两个过程,当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机,就会丢失数据
- 可能阻塞下一个命令:因为执行命令和将命令写入日志都是在主进程完成的,也就是说这两个操作是同步的
其实这两个风险都有一个共性,都跟「AOF 日志写回硬盘的时机」有关
三种写回策略
Redis 写入 AOF 日志的过程:
- 执行完写操作命令后,将命令追加到
server.aof_buf
缓冲区 - 通过
write()
系统调用,将aof_buf
缓冲区的数据写入到文件(此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘)
当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区 page cache 中,然后排入队列,由内核决定何时写入硬盘
可以调用 fsync()
函数要求内核将缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回
Redis 提供了 3 种写回策略(redis.conf
文件的 appendfsync
配置项):
always
:「总是」,每次写入 AOF 文件数据后,就执行fsync()
everysec
:「每秒」,创建一个异步任务,每隔一秒执行fsync()
no
:「从不」,不调用fsync()
,交给操作系统控制写回的时机
这 3 种写回策略都都是在「主进程阻塞」和「减少数据丢失」之间做权衡:
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,最大程度保证数据不丢失 | 每个写命令同步写入硬盘,性能开销大 |
Everysec | 每秒写回 | 性能适中 | 宕机丢失一秒内的数据 |
No | 由操作系统决定 | 性能高 | 宕机丢失的数据可能很多 |
需要根据业务场景选择合适的写回策略
AOF 重写机制
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件会越来越大。当文件过大就会带来性能问题,比如重启 Redis 后,读取大文件,整个恢复过程就会很慢
为了避免 AOF 文件越写越大,提供了 AOF 重写机制。当文件大小超过所设定的阈值,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件
重写的过程是:读取当前数据库中的所有键值对,将每一个键值对用一条命令记录到「新的 AOF 文件」,全部记录完成后,用新的 AOF 文件替换掉现有的 AOF 文件
即:新的 AOF 文件根据键值对的最新状态,用一条命令去记录,代替之前记录这个键值对的多条命令
为什么不直接复用现有的 AOF 文件,而是先写到新的 AOF 文件再覆盖过去?
如果复用,当 AOF 重写过程中失败了,现有的 AOF 文件就会被污染,无法恢复
AOF 后台重写
写入 AOF 日志是在主进程完成的,因为它写入的内容不多,一般不太影响命令的操作
而触发 AOF 重写时,需要读取所有缓存的键值对,并为每个键值对生成一条命令并写入文件,这个过程比较耗时的
所以,Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,好处是:
- 子进程进行 AOF 重写期间,不阻塞主进程
- 子进程带有主进程的数据副本
- 使用子进程而不是线程,因为如果是使用线程,多线程之间共享内存,在修改共享内存数据时,需要通过加锁保证数据的安全,这样就会降低性能
- 子进程怎么拥有和主进程一样的数据副本?—— 写时复制技术
子进程重写过程中,主进程依然可以正常处理命令,如果此时主进程修改了已经存在 key-value,就会发生写时复制
还有个问题,重写 AOF 日志过程中,如果主进程修改了已存在 key-value,此 key-value 数据可能已经被 AOF 重写进了新的文件,这时要怎么办?
Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用
在 bgrewriteaof 子进程执行 AOF 重写期间,当收到写命令时,主进程需要:
- 执行客户端发来的命令
- 将执行后的写命令追加到「AOF 缓冲区」
- 将执行后的写命令追加到「AOF 重写缓冲区」
子进程进行 AOF 重写的工作:
- 扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到新的 AOF 的文件
当子进程完成 AOF 重写工作后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的
主进程收到该信号,会调用一个信号处理函数,该函数主要做以下工作:
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致
- 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件
信号函数执行完,AOF 重写完成
AOF 后台重写会导致阻塞父进程的情况:
- 创建子进程时,要复制父进程的页表等数据,阻塞时间跟页表大小有关
- 创建完子进程后,如果子进程或父进程修改了共享数据,就会发生写时复制,会拷贝物理内存,阻塞时间跟内存数据大小有关
- 信号处理函数执行时也会对主进程造成阻塞
在其他时候,AOF 后台重写都不会阻塞主进程
写时复制技术
写时复制技术(Copy-On-Write, COW)
一个进程通过 fork
系统调用生成子进程时,操作系统会把主进程的「页表」复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不是复制物理内存。也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个
这样一来,子进程就共享了父进程的物理内存数据,页表对应的页表项属性会标记该物理内存的权限为只读
当父进程或子进程在向这个内存发起写操作时,CPU 就会触发写保护中断,操作系统会在「写保护中断处理函数」里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为「写时复制(Copy On Write)」
好处:
- 减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞父线程的
- 节省内存空间:只有在写时才会进行复制
在极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍
总结
Redis 持久化技术中的 AOF 方法,是每执行一条写操作命令,就将该命令追加写入到 AOF 文件。在恢复时,以逐一执行 AOF 文件命令的方式进行数据恢复
Redis 提供了三种将 AOF 日志写回硬盘的策略,分别是 Always
、Everysec
、No
,这三种策略在可靠性上是从高到低,而在性能上则是从低到高
随着执行的写命令越多,AOF 文件的体积也越大,为了避免日志文件过大, Redis 提供了 AOF 重写机制,它会直接扫描数据库中所有的键值对数据,为每一个键值对生成一条写操作命令,并将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可以使得主进程继续正常处理命令
用 AOF 日志的方式来恢复数据其实是很慢的,因为 Redis 执行命令由单线程负责,而 AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个「重放」的过程就会很长