Redis作为一款被广泛应用的内存数据库,想必大家都用过,而作为内存数据库,其持久化机制是确保数据安全和稳定性的关键所在。
想象一下,当你的应用突然断电或服务器发生故障时,如果没有持久化,那些宝贵的数据就可能瞬间消失,那么这样的数据库谁还会去使用呢?
因此,了解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
RDB 是 Redis 默认的持久化方式(AOF默认是关闭的),它将 Redis 在内存中的数据写入到硬盘中,生成一个快照文件。
快照文件是一个二进制文件,包含了 Redis 在某个时间点内的所有数据。
RDB的优点是快速、简单,适用于大规模数据备份和恢复。
但是,RDB也有缺点,例如数据可能会丢失,因为 Redis 只会在指定的时间点生成快照文件。如果在快照文件生成之后,但在下一次快照文件生成之前服务器宕机,那么这期间的数据就会丢失。
如下图,T2 时刻如果服务器宕机,则 k3 和 k4 键的数据可能会丢失。
图片
由于 RDB 文件是以二进制格式保存的,因此它非常紧凑,并且在 Redis 重启时可以迅速地加载数据。相比于AOF,RDB文件一般会更小。
RDB 持久化触发有两种方式:自动 和 手动。
手动:手动方式通过 save 命令或 bgsave 命令进行。
自动:自动方式则是在配置文件中设置,满足条件时自动触发。
图片
手动方式
- save:save 命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。
- bgsave:bgsave 命令会 fork 一个子进程(注意是子进程,不是子线程)在后台生成快照文件,不会阻塞 Redis 服务器,服务器进程(父进程)可以继续处理命令请求。
bgsave命令执行期间,客户端发送的 save 和 bgsave 命令会被拒绝,这样的目的是为了防止父进程和子进程之间产生竞争。
自动方式
自动方式是指通过服务器配置文件的 save 选项,来让 Redis 每隔一段时间自动执行 bgsave ,本质上还是通过 bgsave 命令去实现。
配置文件的 save 选项允许配置多个条件,只要其中任何一个条件满足,就会触发 bgsave。
即:"N 秒内数据集至少有 M 个改动" 这一条件被满足时。
举个例子,如果我们向服务器提供以下配置:
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,bgsave 命令就会被执行:
- 服务器在 900秒 之内,对数据库进行了至少 1次 修改。
- 服务器在 300秒 之内,对数据库进行了至少 10次 修改。
- 服务器在 60秒 之内,对数据库进行了至少 10000次 修改。
如果用户没有主动设置 save 选项,那么服务器会为 save 选项设置默认条件:
save 900 1
save 300 10
save 60 0000
知道上面这些之后我们还要解决两个问题:
问题一:save 选项配置,Redis是通过什么来判断的?
Redis 服务器内部维护了一个计数器:dirty 和一个时间戳:lastsave。
- dirty计数器:dirty计数器记录距离上一次成功执行 save 命令或者 bgsave 命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作),命令修改了多少次数据库,dirty计数器的值就增加多少。
- lastsave时间戳:lastsave时间戳则记录了服务器上一次成功执行 save 命令或者 bgsave 命令的时间。
举个例子,某一时刻,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 的时间。
fork 函数与写时复制
在 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函数有两点注意:
- fork 的这个过程主进程是阻塞的,fork 完之后不阻塞。
- 当数据集比较大的时候,fork 的过程是非常耗时的,过程可能会持续数秒,可能会因为数据量大而导致主进程长时间被挂起,造成Redis服务不可用。
RDB 相关配置
以下是一些 RDB 的相关参数配置:
- save:指定 RDB 持久化操作的条件。当 Redis 的数据发生变化,并且经过指定的时间(seconds)和变化次数(changes)后,Redis 会自动执行一次 RDB 操作。例如,save 3600 10000 表示如果 Redis 的数据在一个小时内发生了至少 10000 次修改,那么 Redis 将执行一次 RDB 操作。
- stop-writes-on-bgsave-error:指定在 RDB 持久化过程中如果出现错误是否停止写入操作。如果设置为 yes,当 Redis 在执行 RDB 操作时遇到错误时,Redis 将停止接受写入操作;如果设置为 no,Redis 将继续接受写入操作。
- rdbcompression:指定是否对 RDB 文件进行压缩。如果设置为 yes,Redis 会在生成 RDB 文件时对其进行压缩,从而减少磁盘占用空间;如果设置为 no,Redis 不会对生成的 RDB 文件进行压缩。
- rdbchecksum:指定是否对 RDB 文件进行校验和计算。如果设置为 yes,在保存 RDB 文件时,Redis 会计算一个 CRC64 校验和并将其追加到 RDB 文件的末尾;在加载 RDB 文件时,Redis 会对文件进行校验和验证,以确保文件没有受到损坏或篡改。
- replica-serve-stale-data:这是 Redis 4.0 中新增的一个配置项,用于指定复制节点在与主节点断开连接后是否继续向客户端提旧数据。当设置为 yes 时,在复制节点与主节点断开连接后,该节点将继续向客户端提供旧数据,直到重新连接上主节点并且同步完全新的数据为止;当设置为 no 时,复制节点会立即停止向客户端提供数据,并且等待重新连接上主节点并同步数据。需要注意的是,当 replica-serve-stale-data 设置为 yes 时,可能会存在一定的数据不一致性问题,因此建议仅在特定场景下使用。
- repl-diskless-sync:这是 Redis 2.8 中引入的一个配置项,用于指定复制节点在进行初次全量同步(即从主节点获取全部数据)时是否采用无盘同步方式。当设置为 yes 时,复制节点将通过网络直接获取主节点的数据,并且不会将数据存储到本地磁盘中;当设置为 no 时,复制节点将先将主节点的数据保存到本地磁盘中,然后再进行同步操作。采用无盘同步方式可以避免磁盘 IO 操作对系统性能的影响,但同时也会增加网络负载和内存占用。
AOF
AOF 持久化是按照 Redis 的写命令顺序将写命令追加到磁盘文件的末尾,是一种基于日志的持久化方式,它保存了 Redis 服务器所有写入操作的日志记录。
AOF 的核心思想是将 Redis 服务器执行的所有写命令追加到一个文件中。当Redis服务器重新启动时,可以通过重新执行 AOF 中的命令来恢复服务器的状态。
图片
AOF 文件解读
一个简单的 AOF 文件示例如下:
图片
这个文件展示了两条命令:
select 0
set k1 hello
其中:
- *号:表示参数个数,后面紧跟着参数的长度和值。
- $号:表示参数长度,后面紧跟着参数的值。
AOF文件中保存的所有命令都遵循相同的格式,即以*开头表示参数个数,$开头表示参数长度,其后紧跟着参数的值。
AOF有个比较好的优势是可以恢复误操作
举个例子,如果你不小心执行了 FLUSHALL 命令,导致数据被误删了 ,但只要 AOF 文件未被重写,那么只要停止服务器,移除 AOF 文件末尾的 FLUSHALL 命令,并重启 Redis ,就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF 的写入与同步
当启用 AOF 时,Redis 发生写命令时其实并不是直接写入到AOF 文件,而是将写命令追加到AOF缓冲区的末尾,之后 AOF缓存区再同步至 AOF文件中。
图片
这行为其实不难理解,Redis 写入命令十分频繁,而 AOF 文件又位于磁盘上,如果每次发生写命令就要操作一次磁盘,性能就会大打折扣。
而 AOF 缓存区同步至 AOF 文件,这一过程由名为 flushAppendonlyFile 的函数完成。
而 flushAppendOnlyFile 函数的行为由服务器配置文件的 appendfsync 选项来决定,该参数有以下三个选项:
- always:每次发生写命令,都同步到 AOF 文件,是最安全的选项。
- everysec:每秒钟同步写入一次到 AOF 文件,在性能和安全之间做了一个平衡。
- no:不主动写入 AOF 文件,何时同步由操作系统来决定。
默认情况下,Redis的 appendfsync 参数为 everysec 。如果需要提高持久化安全性,可以将其改为 always ,如果更关注性能,则可以将其改为 no。但是需要注意的是,使用 no 可能会导致数据丢失的风险,建议在应用场景允许的情况下谨慎使用。
AOF 重写
上面我们讲了 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 文件进行任何读取、分析或者写入操作。
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
127.0.0.1:6379> BGREWRITEAOF
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started by pid 1234
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重写期间,父进程还是在继续接收和处理命令的。
那么就存在一个问题:新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
图片
如上图,T1 时刻重写前数据库存储的键只有 k1和k2,T2时刻发生重写,在T3时刻重写期间,客户端新写入了两个键:k3和k4。T4时刻重写结束。
可以观察到,T4时刻重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1和k2的两个键的数据,而服务器数据库现在却有k1、k2、k3、k4 四个键。
AOF重写缓存区
为了解决这种数据不一致问题,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重写缓存区不会)。
AOF相关配置
在 Redis 的配置文件 redis.conf 中,可以通过以下配置项来设置 AOF 相关参数:
- appendonly:该配置项用于开启或关闭 AOF,默认为关闭。若开启了 AOF,Redis 会在每次执行写命令时,将命令追加到 AOF 文件末尾。
- appendfilename:用于设置 AOF 文件名,默认为 appendonly.aof。
- appendfsync:该配置项用于设置 AOF 的同步机制。有三种可选值:
- always:表示每个写命令都要同步到磁盘,安全性最高,但是性能较差。
- everysec:表示每秒同步一次,是默认选项,既能保证数据安全,又具有较好的性能。
- no:表示不进行同步,而是由操作系统决定何时将缓冲区中的数据同步到磁盘上,性能最好,但是安全性较低。
- auto-aof-rewrite-percentage和auto-aof-rewrite-min-size:这两个配置项用于设置 AOF 重写规则。当 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-use-rdb-preamble:Redis 4版本新特性,混合持久化。AOF重写期间是否开启增量式同步,该配置项在AOF重写期间是否使用RDB文件内容。默认是no,如果设置为yes,在AOF文件头加入一个RDB文件的内容,可以尽可能的减小AOF文件大小,同时也方便恢复数据。
AOF 文件修复
服务器可能在程序正在对 AOF 文件进行写入时停机,造成 AOF 文件损坏。
发生这种情况时,可以使用 Redis 自带的 redis-check-aof 程序,对 AOF 文件进行修复,命令如下:
$ redis-check-aof –fix
AOF 写后日志
我们比较熟悉的是数据库的写前日志(Write Ahead Log,WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。
比如 MySQL Innodb 存储引擎中的 redo log(重做日志)便是采用写前日志。
不过,AOF 日志却正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志。
思考:为什么要这样设计?
其实为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况,省下了语法检查的性能开销。
除此之外,AOF 写后日志还有一个好处:它是在命令执行后才记录日志,所以并不会阻塞当前的写操作。
不过,写后日志也有两个潜在的风险:
- 首先,如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。
- 其次,写后日志虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
混合持久化
在过去, Redis 用户通常会因为 RDB 持久化和 AOF 持久化之间不同的优缺点而陷入两难的选择当中:
- 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
合适的持久化方式
当你想选择适合你的应用程序的持久化方式时,你需要考虑以下两个因素:
- 数据的实时性和一致性:如果对数据的实时性和一致性有很高的要求,则AOF可能是更好的选择。如果对数据的实时性和一致性要求不太高,并且希望能快速地加载数据并减少磁盘空间的使用,那么RDB可能更适合你的应用程序。因为RDB文件是二进制格式的,结构非常紧凑,所以在Redis重启时可以迅速地加载数据。
- Redis的性能需求:如果对Redis的性能有很高的要求,那么关闭持久化功能也是一个选择。因为持久化功能可能会影响Redis的性能,但是一般不建议这么做。
总结
本篇文章到这就结束了,最后我们来做个小总结:
我们要意识到Redis的持久化机制扮演着至关重要的角色。RDB和AOF两种主要的持久化方式各有其优势和使用场景。
RDB通过提供特定时间点的数据快照,对于灾难恢复是非常有效的;而AOF则通过记录每个写入操作,提供了更好的数据持久性保证。然而,它们也有各自的局限性,这就需要根据实际需求来权衡选用哪种持久化方式。
最后,不可忽视的是,在选择合适的持久化策略时,我们还应考虑如何平衡内存使用、磁盘使用、性能与持久性等多个因素。只有对Redis持久化的深入理解,我们才能充分利用其强大的功能,以满足各种业务需求。