本文字数:8059字,阅读大约需要25分钟。
Redis作为一款被广泛应用的内存数据库,想必大家都用过,而作为内存数据库,其持久化机制是确保数据安全和稳定性的关键所在。
想象一下,当你的应用突然断电或服务器发生故障时,如果没有持久化,那些宝贵的数据就可能瞬间消失,那么这样的数据库谁还会去使用呢?
因此,了解Redis持久化的原理,对于Redis保障数据的完整性是至关重要的,这也是为什么面试中经常会涉及到Redis持久化的问题。
这篇文章就跟各位一起来学习下Redis的持久化机制。
Redis持久化有两种方式:RDB(Redis DataBase)和AOF(Append Only File)。
RDB:RDB文件是一个经过压缩的二进制文件。
AOF:AOF则是以追加的方式记录Redis执行的每一条写命令。
RDB 和 AOF 是可以同时开启的,在这种情况下,当Redis重启的时候会优先载入 AOF 文件来恢复原始的数据。
接下来,我会分别介绍 RDB 和 AOF 的实现原理。
RDB 是 Redis 默认的持久化方式(AOF默认是关闭的),它将 Redis 在内存中的数据写入到硬盘中,生成一个快照文件。
快照文件是一个二进制文件,包含了 Redis 在某个时间点内的所有数据。
RDB的优点是快速、简单,适用于大规模数据备份和恢复。
但是,RDB也有缺点,例如数据可能会丢失,因为 Redis 只会在指定的时间点生成快照文件。如果在快照文件生成之后,但在下一次快照文件生成之前服务器宕机,那么这期间的数据就会丢失。
如下图,T2 时刻如果服务器宕机,则 k3 和 k4 键的数据可能会丢失。
由于 RDB 文件是以二进制格式保存的,因此它非常紧凑,并且在 Redis 重启时可以迅速地加载数据。相比于AOF,RDB文件一般会更小。
RDB 持久化触发有两种方式:自动 和 手动。
手动:手动方式通过 save
命令或 bgsave
命令进行。
自动:自动方式则是在配置文件中设置,满足条件时自动触发。
手动方式
bgsave命令执行期间,客户端发送的 save 和 bgsave 命令会被拒绝,这样的目的是为了防止父进程和子进程之间产生竞争。
自动方式
自动方式是指通过服务器配置文件的 save 选项,来让 Redis 每隔一段时间自动执行 bgsave ,本质上还是通过 bgsave 命令去实现。
配置文件的 save 选项允许配置多个条件,只要其中任何一个条件满足,就会触发 bgsave。
即:"N 秒内数据集至少有 M 个改动" 这一条件被满足时。
举个例子,如果我们向服务器提供以下配置:
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,bgsave 命令就会被执行:
如果用户没有主动设置 save 选项,那么服务器会为 save 选项设置默认条件:
save 900 1
save 300 10
save 60 0000
知道上面这些之后我们还要解决两个问题:
问题一:save 选项配置,Redis是通过什么来判断的?
Redis 服务器内部维护了一个计数器:dirty 和一个时间戳:lastsave。
举个例子,某一时刻,dirty 计数器和 lastsave 时间戳的值如下:
dirty:200
lastsave:1703952000000
表示服务器在上次保存之后(2023-12-31 00:00:00),对数据库状态共进行了 200 次修改。
通过这种方式来判断条件是否满足。
问题二:Redis 判断的时机是什么时候?
Redis通过周期性操作函数 serverCron 默认每隔100毫秒就会执行一次检查,来判断 save 选项所设置的保存条件是否已经满足。
serverCron 会遍历所有保存条件,只要有任意一个条件被满足,就执行 bgsave 命令。
bgsave 执行完成之后,Redis会重置 dirty 和 lastsave 的值。dirty 重置为0,lastsave 更新为上次 bgsave 的时间。
在 Redis 中,bgsave命令使用 fork 函数创建子进程生成RDB时,允许父进程接收和处理写命令。
那 Redis 是如何实现 一边处理写请求,同时生成RDB文件的呢?
Redis 使用操作系统的 写时复制技术 COW(Copy On Write) 来实现快照的持久化。
Redis 的使用场景中通常有大量的读操作和较少的写操作,而 fork 函数可以利用 Linux 操作系统的写时复制(Copy On Write,即 COW)机制,让父子进程共享内存,从而减少内存占用,并且避免了没有必要的数据复制。
随带一提:JDK的 CopyOnWriteArrayList 和 CopyOnWriteArraySet 容器也使用到了写时复制技术。
我们可以使用 Linux下的 man fork
命令来查看下 fork 函数的说明文档。
翻译如下:
在Linux下,fork()是使用写时复制的页实现的,所以它唯一的代价是复制父进程的页表以及为子进程创建独特的任务结构所需的时间和内存。
简单来说就是 fork()
函数会复制父进程的地址空间到子进程中,复制的是指针,而不是数据,所以速度很快。
当没有发生写的时候,子进程和父进程指向地址是一样的,父子进程共享内存空间,这时可以将父子进程想象成一个连体婴儿,共享身体。
直到发生写的时候,系统才会真正拷贝出一块新的内存区域,读操作和写操作在不同的内存空间,子进程所见到的最初资源仍然保持不变,从而实现父子进程隔离。
此做法的主要优点是如果期间没有写操作,就不会有副本被创建。
示意图如下:
通过使用 fork 函数和写时复制机制,Redis 可以高效地执行 RDB 持久化操作,并且不会对 Redis 运行过程中的性能造成太大的影响。
同时,这种方式也提供了一种简单有效的机制来保护 Redis 数据的一致性和可靠性。
不过,fork函数有两点注意:
以下是一些 RDB 的相关参数配置:
AOF 持久化是按照 Redis 的写命令顺序将写命令追加到磁盘文件的末尾,是一种基于日志的持久化方式,它保存了 Redis 服务器所有写入操作的日志记录。
AOF 的核心思想是将 Redis 服务器执行的所有写命令追加到一个文件中。当Redis服务器重新启动时,可以通过重新执行 AOF 中的命令来恢复服务器的状态。
一个简单的 AOF 文件示例如下:
这个文件展示了两条命令:
select 0
set k1 hello
其中:
AOF文件中保存的所有命令都遵循相同的格式,即以*开头表示参数个数,$开头表示参数长度,其后紧跟着参数的值。
AOF有个比较好的优势是可以恢复误操作
举个例子,如果你不小心执行了 FLUSHALL
命令,导致数据被误删了 ,但只要 AOF 文件未被重写,那么只要停止服务器,移除 AOF 文件末尾的 FLUSHALL
命令,并重启 Redis ,就可以将数据集恢复到 FLUSHALL
执行之前的状态。
当启用 AOF 时,Redis 发生写命令时其实并不是直接写入到AOF 文件,而是将写命令追加到AOF缓冲区的末尾,之后 AOF缓存区再同步至 AOF文件中。
这行为其实不难理解,Redis 写入命令十分频繁,而 AOF 文件又位于磁盘上,如果每次发生写命令就要操作一次磁盘,性能就会大打折扣。
而 AOF 缓存区同步至 AOF 文件,这一过程由名为 flushAppendonlyFile
的函数完成。
而 flushAppendOnlyFile
函数的行为由服务器配置文件的 appendfsync
选项来决定,该参数有以下三个选项:
默认情况下,Redis的 appendfsync
参数为 everysec
。如果需要提高持久化安全性,可以将其改为 always
,如果更关注性能,则可以将其改为 no
。但是需要注意的是,使用 no
可能会导致数据丢失的风险,建议在应用场景允许的情况下谨慎使用。
上面我们讲了 AOF 是通过追加命令的方式去记录数据库状态的,那么当随着服务器运行时间的流逝,AOF 文件可能会越来越大,达到几G甚至几十个G。
过大的 AOF 文件会对 Redis 服务器甚至宿主机造成影响,并且 AOF 越大,使用 AOF 来进行数据恢复所需的时间也就越多。
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 文件重写(rewrite)机制。
Redis的 AOF 重写机制指的是将 AOF 文件中的冗余命令删除,以减小 AOF 文件的大小并提高读写性能的过程。
通过该功能,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但新 AOF 文件不会包含任何浪费空间的冗余命令,所以新 AOF 文件的体积通常会比旧 AOF 文件的体积要小得多。
虽然叫做AOF重写,但实际上,AOF 文件重写并不需要对现有的AOF 文件进行任何读取、分析或者写入操作。
AOF重写是通过读取服务器当前的数据库状态来实现的。
我举个例子大家就明白了,假设我对 Redis 执行了下面六条命令:
rpush list "A"
rpush list "B"
rpush list "C"
rpush list "D"
rpush list "E"
rpush list "F"
那么服务器为了保存当前 list键 的状态,会在AOF文件中写入上述六条命令。
而我现在要对 AOF 进行重写的话,其实最高效最简单的方式不是挨个读取和分析现有AOF文件中的这六条命令。
而是直接从数据库中读取键 list 的值,然后用一条命令:rpush list "A" "B" "C" "D" "E" "F"
,可以直接代替原 AOF 文件中的六条命令。
命令由六条减少为一条,重写的目的就达到了。
Redis的AOF重写机制采用了类似于复制的方式,首先将内存中的数据快照保存到一个临时文件中,然后遍历这个临时文件,只保留最终状态的命令,生成新的AOF文件。
具体来说,Redis执行AOF重写可以分为以下几个步骤:
Redis提供了手动触发AOF重写的命令 BGREWRITEAOF
,重写过程是由父进程 fork 出来的子进程来完成的,期间父进程可以继续处理请求。
可以在Redis的客户端中执行该命令来启动AOF重写过程。Redis 2.2 需要自己手动执行 BGREWRITEAOF
命令,到了 Redis 2.4 则可以自动触发 AOF 重写。
具体操作步骤如下:
打开redis-cli命令行工具,连接到Redis服务。
执行BGREWRITEAOF
命令,启动AOF重写过程。
$ redis-cli
127.0.0.1:6379> BGREWRITEAOF
Redis会返回一个后台任务的ID,表示AOF重写任务已经开始。
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started by pid 1234
可以使用 INFO PERSISTENCE
命令查看当前AOF文件的大小和重写过程的状态,等待重写完成即可。
127.0.0.1:6379> INFO PERSISTENCE
# Persistence
aof_enabled:1
aof_rewrite_in_progress:1
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:0
aof_current_rewrite_time_sec:14
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
需要注意的是,即使手动触发AOF重写,Redis也会在满足一定条件时自动触发AOF重写,以保证AOF文件的大小和性能。
重写规则通过配置中的 auto-aof-rewrite-percentage
和 auto-aof-rewrite-min-size
选项控制。
子进程在AOF重写期间,父进程还是在继续接收和处理命令的。
那么就存在一个问题:新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
如上图,T1 时刻重写前数据库存储的键只有 k1和k2,T2时刻发生重写,在T3时刻重写期间,客户端新写入了两个键:k3和k4。T4时刻重写结束。
可以观察到,T4时刻重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1和k2的两个键的数据,而服务器数据库现在却有k1、k2、k3、k4 四个键。
为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区。
AOF重写缓存区在AOF重写时开始启用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。
示意图如下:
由了AOF重写缓存区的存在,当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用处理函数,将AOF重写缓冲区中的所有内容写入到新AOF文件中(就是重写后的文件),这样重写后数据库状态就和服务器当前的数据库状态一致了。
这里有个注意的点,AOF重写缓存区同步至AOF文件中(上述红色箭头),这个过程是同步的,会阻塞父进程,在其他时候,AOF后台重写都不会阻塞父进程。
然后会对新的AOF文件进行改名,覆盖现有的AOF文件,至此完成新旧两个AOF文件的替换。
AOF重写期间,命令会同步至AOF缓存区和AOF重写缓冲区,那么可不可以使用AOF缓存区代替AOF重写缓冲区呢?
思考:AOF缓冲区可以替代AOF重写缓冲区吗?
先说结论:AOF缓冲区不可以替代AOF重写缓冲区。
原因是AOF重写缓冲区记录的是从重写开始后的所有需要重写的命令,而AOF缓冲区可能只记录了部分的命令(如果写回的话,AOF缓存区的数据就会失效被丢失,因而只会保存一部分的命令,而AOF重写缓存区不会)。
在 Redis 的配置文件 redis.conf 中,可以通过以下配置项来设置 AOF 相关参数:
auto-aof-rewrite-min-size
设置的值,并且 AOF 文件增长率达到 auto-aof-rewrite-percentage
所定义的百分比时,Redis 会启动 AOF 重写操作。
auto-aof-rewrite-percentage
默认值为100, auto-aof-rewrite-min-size
默认值为64mb。
Redis会记录上次重写时的AOF大小,也就是说默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。服务器可能在程序正在对 AOF 文件进行写入时停机,造成 AOF 文件损坏。
发生这种情况时,可以使用 Redis 自带的 redis-check-aof 程序,对 AOF 文件进行修复,命令如下:
$ redis-check-aof –fix
我们比较熟悉的是数据库的写前日志(Write Ahead Log,WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。
比如 MySQL Innodb 存储引擎中的 redo log(重做日志)便是采用写前日志。
不过,AOF 日志却正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。
思考:为什么要这样设计?
其实为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况,省下了语法检查的性能开销。
除此之外,AOF 写后日志还有一个好处:它是在命令执行后才记录日志,所以并不会阻塞当前的写操作。
不过,写后日志也有两个潜在的风险:
在过去, Redis 用户通常会因为 RDB 持久化和 AOF 持久化之间不同的优缺点而陷入两难的选择当中:
为了让用户能够同时拥有上述两种持久化的优点, Redis 4.0 推出了一个“鱼和熊掌兼得”的持久化方案 —— RDB-AOF 混合持久化。
这种持久化能够通过 AOF 重写操作创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态。至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。
也就是说当开启混合持久化之后,AOF文件中的内容:前半部分是二进制的RDB内容,后面跟着AOF增加的数据,AOF位于两次RDB之间。
格式会类似下面这样:
在目前版本中, RDB-AOF 混合持久化功能默认是处于关闭状态的, 要启用该功能, 用户不仅需要开启 AOF 持久化功能, 还需要将 aof-use-rdb-preamble
选项的值设置为 true。
appendonly yes
aof-use-rdb-preamble yes
当你想选择适合你的应用程序的持久化方式时,你需要考虑以下两个因素:
本篇文章到这就结束了,最后我们来做个小总结:
我们要意识到Redis的持久化机制扮演着至关重要的角色。RDB和AOF两种主要的持久化方式各有其优势和使用场景。
RDB通过提供特定时间点的数据快照,对于灾难恢复是非常有效的;而AOF则通过记录每个写入操作,提供了更好的数据持久性保证。然而,它们也有各自的局限性,这就需要根据实际需求来权衡选用哪种持久化方式。
最后,不可忽视的是,在选择合适的持久化策略时,我们还应考虑如何平衡内存使用、磁盘使用、性能与持久性等多个因素。只有对Redis持久化的深入理解,我们才能充分利用其强大的功能,以满足各种业务需求。
希望这篇文章能给你带来收获和思考,如果你也有可借鉴的经验和深入的思考,欢迎评论区留言讨论。