AOF持久化与RDB日志快照
-
redis里的 AOF持久化功能,是保持写操作到日志的持久化方式,注意只会记录写操作命令,读操作命令是不会被记录的,因为没有意义
-
在redis中AOF持久化功能默认是不开启的,需要修改redis.conf配置文件中的参数:
-
appendonly yes (默认no,关闭)表示是否开启AOF持久化:
-
appendfilename “appendonly.aof” AOF持久化配置文件的名称:
-
Redis是先执行写操作命令后,才将该命令记录到AOF日志里,这么做的好处是什么?有什么潜在风险?
-
避免额外的检查开销
保证了记录在AOF日志里的命令都是可执行并且正确的
-
不会阻塞当前写操作命令的执行
-
潜在风险:
- 执行写操作命令和记录日志是两个过程,那么当redis还没来得及将命令写入到硬盘时,服务器宕机了,这个数据就会有 丢失风险
- 由于写操作命令执行成功后才记录到AOF日志,所以不会阻塞当前写操作命令,但是 可能会给(下一个)命令带来阻塞风险
- 如果将日志内容写入到硬盘时,服务器的硬盘的I/O压力太大,就会导致写硬盘的速度很慢,进而阻塞住了,就会导致后续的命令无法执行
三种写回策略
redis写入AOF日志的过程:
- redis执行完写操作后,会将命令追加到server.aof_buf缓冲区;
- 然后通过write()系统调用,将aof_buf缓冲区的数据写入到AOF文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘
- 具体内核缓冲区的数据什么时候写入硬盘,由内核决定
redis提供了三种写回硬盘的策略
在redis.conf配置文件中的appendfsync配置项可以有以下:
- Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- 可以最大程度保证数据不丢失,但是由于每执行一条写操作命令就同步将AOF内容写回硬盘,所以是不可避免回影响主进程的功能;
- 策略是每次写入AOF文件数据后,就执行fsync()函数
- 业务场景:高可靠
- Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
- 避免了 Always 策略的性能开销,也比 No 策略更能避免数据丢失,当然如果上一秒的写操作命令日志没有写回到硬盘,发生了宕机,这一秒内的数据自然也会丢失。
- 策略是创建一个异步任务执行fsync()函数
- 业务场景:允许数据丢失一点,但又想性能高
- No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
- 是交由操作系统来决定何时将AOF日志内容写回硬盘,操作系统写回硬盘的时机是不可预知的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。
- 策略是永不执行fsync()函数
- 业务场景:追求高性能
AOF重写机制
Redis为了避免AOF文件越写越大,提供了AOF重写机制,当AOF文件的大小超过所设定的阈值后,Redis就会启动AOF重写机制,来压缩AOF文件。
举个例子,在没有使用重写机制前,假设前后执行了「set name xiaolin」和「set name xiaolincoding」这两个命令的话,就会将这两个命令记录到 AOF 文件。

但是在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。这样一来,一个键值对在重写日志中只用一条命令就行了。
重写工作完成后,就会将新的 AOF 文件覆盖现有的 AOF 文件,这就相当于压缩了 AOF 文件,使得 AOF 文件体积变小了。
然后,在通过 AOF 日志恢复数据时,只用执行这条命令,就可以直接完成这个键值对的写入了。
重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。最后在重写工作完成后,将新的 AOF 文件覆盖现有的 AOF 文件。
为什么重写AOF的时候,不直接复用现有的AOF文件,而是写到新的AOF文件再覆盖过去
因为如果 AOF 重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。
所以 AOF 重写过程,先重写到新的 AOF 文件,重写失败的话,就直接删除这个文件就好,不会对现有的 AOF 文件造成影响。
AOF后台重写
Redis 的重写 AOF 过程是由后台子进程 *bgrewriteaof* 来完成的,有两个好处:
- 子进程进行AOF重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程
- 子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
子进程是怎么拥有主进程一样的数据副本的呢?
RDB快照
RDB内容是二进制数据,所谓的快照就是记录某一个瞬间东西,所以RDB快照就是记录某一个瞬间的内存数据,记录的是实际数据,而AOF文件记录的是命令操作的日志,而不是实际数据
因此,在Redis恢复数据时,RDB恢复数据的效率会比AOF快一些,因为直接将RDB文件读入内存就好了,不需要像AOF一样还需要额外执行操作命令的步骤才能恢复数据
快照怎么用
redis提供了两个命令来生成RDB文件,分别是save和bgsave,他们的区别就在于是否在主线程里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsava 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB文件的加载工作是在服务启动时自动执行的,Redis并没有提供专门用于加载RDB文件的命令
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsava 命令,默认会提供以下配置:
save 900 1
save 300 10
save 60 10000
别看选项名叫 sava,实际上执行的是 bgsava 命令,也就是会创建子进程来生成 RDB 快照文件。
只要满足上面条件的任意一个,就会执行 bgsava,它们的意思分别是:
- 900 秒之内,对数据库进行了至少 1 次修改;
- 300 秒之内,对数据库进行了至少 10 次修改;
- 60 秒之内,对数据库进行了至少 10000 次修改。
Redis的快照是全量快照,也就是一说每次执行快照,都是把内存中的
所有数据都记录到磁盘中执行快照是一个比较重的操作,如果频率太频繁,可能会对Redis性能产生影响,如果频率太低,服务器故障时,丢失的数据会更多
通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。
执行bgsave快照是,数据能被修改吗
执行 bgsava 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。
那具体如何做到到呢?关键的技术就在于写时复制技术(*Copy-On-Write, COW*)。
- 执行 bgsava 命令的时候,会通过
fork()创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。 - 只有在发生修改内存数据的情况时,物理内存才会被复制一份
- 这样的目的是为了减少创建子进程是的性能损耗,从而加快创建子进程的速度,笔记创建子进程的过程中,会阻塞主线程
AOF和RDB结合使用
如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:
aof-use-rdb-preamble yes
混合持久化工作在 AOF 日志重写过程。
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
Redis主从复制
这个模式可以保证多台服务器的数据一致性,且主从服务器之间采用的是读写分离的方式
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
第一次同步
多台服务器之间要通过什么方式来确定谁是主服务器,或者谁是从服务器呢?
我们可以使用 replicaof(Redis 5.0 之前使用 slaveof)命令形成主服务器和从服务器的关系。
比如,现在有服务器 A 和 服务器 B,我们在服务器 B 上执行下面这条命令:
# 服务器 B 执行这条命令
replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>
接着,服务器 B 就会变成 服务器 A 的「从服务器」,然后与主服务器进行第一次同步。
主从服务器间的第一次同步的过程可分为三个阶段:
-
第一阶段是建立链接、协商同步;
执行了 replicaof 命令后,从服务器就会给主服务器发送
psync命令,表示要进行数据同步。psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。
- runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 run ID,所以将其设置为 "?"。
- offset,表示复制的进度,第一次同步时,其值为 -1。
主服务器收到 psync 命令后,会用
FULLRESYNC作为响应命令返回给对方。并且这个响应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制进度 offset。从服务器收到响应后,会记录这两个值。
FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。
所以,第一阶段的工作时为了全量复制做准备。
-
第二阶段是主服务器同步数据给从服务器;
接着,主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。
从服务器收到 RDB 文件后,会先清空当前的数据,然后载入 RDB 文件。
这里有一点要注意,主服务器生成 RDB 这个过程是不会阻塞主线程的,也就是说 Redis 依然可以正常处理命令。
但是这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。
那么为了保证主从服务器的数据一致性,主服务器会将在 RDB 文件生成后收到的写操作命令,写入到 replication buffer 缓冲区里。
-
第三阶段是主服务器发送新写操作命令给从服务器。
在主服务器生成的 RDB 文件发送后,然后将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,然后从服务器重新执行这些操作。
命令传播
基于长连接的命令传播,通过这种方式来保证第一次同步后的主从服务的数据一致性
主从服务器在完成第一次同步后,双方之间就会维护一个TCP连接,后续主服务器可以通过这个连接继续将写操作命令传播给服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。而且这个连接是长连接的,目的是避免频繁的TCP连接和断开带来的性能开销
分摊主服务器的压力
主服务器会做两件耗时的操作:生成RDB文件和传输RDB文件
主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,就会带来两个问题:
- 由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;
- 传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。
我们在「从服务器」上执行下面这条命令,使其作为目标服务器的从服务器:
replicaof <目标服务器的IP> 6379
此时如果目标服务器本身也是「从服务器」,那么该目标服务器就会成为「经理」的角色,不仅可以接受主服务器同步的数据,也会把数据同步给自己旗下的从服务器,从而减轻主服务器的负担。
增量复制
如果此时断开的网络,又恢复正常了,要怎么继续保证主从服务器的数据一致性呢?
从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
主要有三个步骤:
- 从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
- 主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
- 然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。
那么关键的问题来了,主服务器怎么知道要将哪些增量数据发送给从服务器呢?
答案藏在这两个东西里:
- repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
- replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
那repl_backlog_buffer 缓冲区是什么时候写入的呢?
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。
网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
- 如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;
- 相反,如果判断出从服务器要读取的数据已经不存在
repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。
当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区我们前面也提到过,它是缓存将要传播给从服务器的命令。
Redis是单线程的进程吗?
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
- Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
- Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大key。
Redis 单线程模式是怎样的?
Redis 采用单线程为什么还这么快?
- Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
- Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
Redis 6.0 之前为什么使用单线程?
使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
Redis 6.0 之后为什么引入了多线程?
在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络请求处理的并行度,Redis 6.0 对于网络请求采用多线程来处理。但是对于读写命令,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。

浙公网安备 33010602011771号