谈谈redis中的持久化机制

为什么要进行持久化

为了避免数据丢失

redis作为一个键值对的内存数据库,数据全存储在内存当中,当遇到进程退出或者服务器重启宕机的情况,内存中的数据就会消失,那这样存在redis中的数据就全丢了,如果业务场景仅仅是缓存,数据丢失影响或许不大,重新去数据库加载下再次写入redis就行了,但是如果把业务数据存储在redis中,拿redis当数据库使用的话,数据的丢失可能是毁灭性的打击,所以为了避免数据丢失,redis提供了持久化的支持,可以选择不同的方式将数据从内存保存到磁盘上面,使数据可以持久化保存。

redis持久化机制

RDB

RDB 是一种快照存储持久化方式,持久化的时候fork一个子进程,通过COW机制生成RDB文件保证持久化

具体就是将某一时刻的内存数据保存为一个快照存储到磁盘中,默认文件名为dump.rdb,在redis服务启动的时候会将dump.rdb文件加载到内存中恢复数据。

RDB触发条件

手动触发

通过在redis服务上面执行以下命令触发

  • save 阻塞当前redis服务,执行save期间,redis服务不能处理其他命令,直到RDB过程完成为止,线上环境不建议使用
  • bgsave 执行该命令时,redis会在后台异步进行快照操作,此时redis服务仍然可以响应客户端请求。
    具体操作是执行该命令时,redis会执行fork操作创建一个子进程,由子进程来负责RDB持久化过程,完成后自动结束。这里需要注意的是在fork的时候需要阻塞主进程,一般时间比较短,但是如果redis服务中的数据量比较大的话,fork时间就会变长,且占用内存会加倍。

自动触发

自动触发是通过redis.conf配置自动触发 通过save “”来停用自动触发RDB

主从同步时,从节点向主节点发起同步请求,主节点收到sync命令后,开始执行bgsave

etc:

1
2
save 60 1 60s内如果 >= 1个key值发生变化则会触发RDB
save 600 10 600s内如果 >= 10个key值发生变化会触发RDB

bgsave流程

  1. 查看是否正在进行RDB或者AOF持久化,是则直接返回
  2. 当前不在进行持久化,fork子进程,子进程和父进程共享内存数据,父进程发生新的写入操作时,会对影响的数据拷贝一个新的内存段(存在疑问),在新的内存段上面进行处理,不影响子进程的数据
  3. 子进程将数据集写入到一个临时的RDB文件中
  4. 子进程对临时RDB文件写入完成后,用新的RDB文件替换老的RDB文件,并删除旧的RDB文件,并通过信号通知父进程

AOF

AOF(Append Only File)是指把每次执行的写命令追加写入到日志中,当需要恢复数据的时候重新执行AOF中的命令就行了。实际上redis每次写入并没有直接写入到日志文件里,而是写入到一个缓冲区(aof_buf)中,而后通过缓冲区同步策略对缓冲区的数据进行落盘操作

AOF执行流程

AOF不需要设置任何触发条件,对redis服务的所有写命令都会记录到AOF文件中

AOF的写入流程可以分为以下3个步骤

  1. 命令追加(append): 将redis的写命令追加到AOF的缓冲区aof_buf
  2. 文件写入(write)和文件同步(fsync):AOF根据策略将aof_buf中的数据同步到磁盘
  3. 文件重写(rewrite):由于AOF文件会越来越大,定期对AOF文件进行重写,从而对写命令进行压缩

命令追加

redis使用单线程处理客户端命令,如果每次一有写命令就写磁盘的话,磁盘IO就成为redis的性能瓶颈了,所以redis会预先将执行的写命令追加(append)到一个缓冲区(aof_buf),而不是直接写入文件

文件写入和文件同步

  1. write()
    为了提高文件写入效率,当用户调用write函数将数据写入文件时,操作系统会先把数据写入到一个内存缓冲区中,当缓冲区被填满或者超过了指定时限时,才真正将缓冲区的数据写入磁盘中
  2. fsync()
    虽然操作系统对write函数进行了优化,但是也带来了安全问题,如果宕机内存缓冲区中的数据会丢失,因此操作系统同时提供了同步函数fsync(),强制操作系统把缓冲区内部的数据写入到磁盘中,从而保证了数据持久化

redis提供了appendfsync配置项来控制AOF缓冲区的文件同步策略,可以配置以下三种策略

  • appendfsync always: 每执行一次命令就保存一次

命令写入aof_buf后立即调用系统函数fsync函数同步到aof文件,fsync操作完成后线程返回,整个过程是阻塞的,这种情况下,每次写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,redis只能支持大约几百TPS写入,严重降低了redis的性能

  • appendfsync no: 不强制保存,由操作系统决定什么时候写入磁盘
    命令写入aof_buf中缓冲区调用系统write操作,不对AOF文件做fsync操作,同步由操作系统负责,通常同步周期为30秒,这种情况下、文件同步时间不可控制,且缓冲区内的数据会很多,数据安全性无法得到保证

  • appendfsync everysec: 每秒钟保存一次
    命令写入aof_buf缓冲区后,调用系统write操作,write完成后立即返回,fsync同步操作由单独的进程每秒调用一次,everysec是前两种策略的折中方案,是性能和数据安全性的平衡,因此也是redis的默认设置,也是比较推崇的选项

文件同步策略 write 阻塞 fsync阻塞 宕机时丢失的数据量
always 阻塞 阻塞 最多只丢失一个命令的数据量
no 阻塞 不阻塞 操作系统最后一次对AOF文件fsync后的数据
everysec 阻塞 不阻塞 一般不超过一秒的数据

文件重写

AOF重写过程提供了手动触发和自动触发两种方式

  • 手动触发: 直接调用bgrewriteaof,执行方式类似于bgsave,fork子进程执行具体的操作,fork时阻塞
  • 自动触发: 使用auto-aof-rewrite-min-size和auto-aof-rewrite-percentage配置项以及aof_current_size和aof_base_size的状态确定触发时机
    • auto-aof-rewrite-min-size: 执行AOF重写时,文件的最小体积,默认64M
    • auto-aof-rewrite-percentage: 执行AOF重写时,当前AOF文件的大小(aof_current_size)和上一次AOF重写时AOF文件大小(aof_base_size)的比值

文件重写流程

这里以手动调用bgrewriteaof为例,叙述下AOF重写的流程

  1. 客户端通过bgrewriteaof对redis主进程发起AOF重写请求

  2. 当前不存在bgsave/bgrewriteaof的子进程时,redis主进程fork子进程(阻塞),如果发现bgrewriteaof子进程则直接返回,如果发现bgsave子进程则等待bgsave操作完成后再fork操作

  3. 主进程fork操作执行完毕后,继续处理其他命令,同时把新的写命令追加到aof_buf中和aof_rewrite_buf缓冲区中

    • 文件重写完成之前,主进程会继续把写命令追加到aof_buf缓冲区,根据appendfsync策略将写命令同步到老的AOF文件内,这样可以避免AOF重写失败造成数据丢失,保证原有AOF文件的正确性
    • 由于fork操作时运用写时复制技术,子进程共享fork操作时的内存数据,主进程会把新命令追加到一个aof_rewrite_buf缓冲区中,避免AOF重写失败造成数据丢失这部分数据
  4. 子进程读取redis进程中的数据快照,生成写入命令后按照命令合并规则批量写入到新的AOF文件

  5. 子进程写完新的AOF文件后,向主进程发信号,主进程更新统计信息,具体可以通过info persistence指令查看

  6. 主进程接收到子进程的写入完成信号后,将aof_rewrite_buf缓冲区的写命令追加到新的AOF文件

  7. 主进程使用新的AOF文件替换旧的AOF文件,AOF重写完成

压缩机制

文件重写之所以能够压缩AOF文件的大小,主要在于以下原因

  • 过期的数据不再写入AOF文件
  • 无效的命令不再写入AOF文件(比如重复的key值设置,set key1 v1 set key1 v2,已经删除的数据)
  • 多条命令可以合并为单个 sadd testset v1 sadd testset v2 sadd testset v3可以合并为 sadd testset v1 v2 v3

RDB、AOF对比

RDB优点

  • 与AOF相比,通过rdb文件恢复数据比较快
  • rdb十分紧凑,适合用来做数据备份
  • 通过RDB进行数据备份,由于使用子进程生成,所以对redis服务器性能影响较小

缺点

  • 如果服务器宕机的话,使用RDB方式会造成某个时间段内的数据丢失,比如设置10分钟同步一次,或者5分钟写入1000次就同步一次,如果在这个过程中服务器宕机,则这个时间段的数据就会丢失
  • 使用save方式会造成服务器阻塞,同步完成后才能响应后续请求
  • 使用bgsave命令的时候,如果内存中的数据太大,fork也会发生阻塞,另外fork子进程会耗费内存
  • redis子进程向磁盘写入数据会带来IO压力

AOF优点:

  • AOF只是追加日志文件,对服务器性能影响较小,速度比RDB要快,消耗的内存较少
    AOF的缺点:
  • AOF生成的日志太大,即使有重写操作,文件体积仍然很大
  • 恢复数据的速度比RDB要慢很多
  • AOF文件重写也是通过fork子进程的方式处理的,存在fork时阻塞问题

RDB还是AOF,如何去选择?

其实策略的选择主要是看业务中对数据丢失的容忍度,如果可以接受十几分钟或者更多的数据丢失,那么就可以选择RDB,性能更好,如果只能接受秒级别的数据丢失,选择AOF方案更为合适

主从复制原理

为什么这里会牵扯到主从复制原理呢?因为主从复制的内容实际上部分内容就是通过RDB来实现的,所以我们在这篇文章一同就给处理了,下面就来讲解下主从复制的原理

如何开启主从复制

  • redis服务启动后,执行slaveof 命令
  • 配置文件配置 slaveof
  • 启动命令后面加 –slaveof

主从复制的开启完全是在从节点发起的,不需要主节点做任何事情

保存主节点信息

保存主节点信息 从节点服务器内部维护了masterhost和masterport两个字段用于存储主节点ip和端口
一般redis节点通过slaveof host port命令来将当前redis服务器变为指定服务器的从属服务器,从而使得从服务器对该主服务器进行复制(实际的复制操作在slaveof命令执行后并返回OK之后才开始进行)

建立socket连接

从节点每秒1次调用复制定时函数replicationCron(),如果发现有主节点可以连接,便会根据主节点的ip和port,创建socket连接,如果连接成功,则:
从节点: 为该socket建立一个专门处理复制工作的文件事件处理器,负责后续复制工作,收接收RDB文件,接收命令传播

主节点:接收到从节点的socket连接后,并将从节点当作是连接到主节点的一个客户端,后续的命令会以从节点向主节点发送命令请求的形式来进行

发送ping命令

从节点向主节点发送ping命令,主要有以下作用

  • 检测主从之间套接字是否可用
  • 检测主节点是否可以接收处理命令

可能有以下三种情况

  • 主节点返回pong,socket 正常,主节点可以处理请求,复制过程继续
  • 超时,一定时间内仍未收到主节点的回复,说明socket连接不可用,从节点断开socket连接并重连
  • 返回pong以外的结果,如果主节点返回其他结果,如果处理正在超时运行的脚本,说明主节点当前无法处理命令,断开socket连接并重连

身份验证

如果从节点设置了masterauth选项,则从节点需要进行身份验证,没有配置则跳过
身份验证是通过从节点向主节点发送auth命令进行的,auth的参数为配置文件中masterauth的值,如果从节点masterauth和主节点requirepass状态一致,则身份验证通过,复制过程继续,不一致,则断开socket连接,并重连

发送从节点端口信息

身份验证通过后,从节点会向主节点发送其监听的端口号,主节点将信息保存到该从节点对应的客户端的slave_listen_port中,这个端口信息是用来在主节点查询主从复制状态的时候显示的端口信息,没什么别的作用(info replication)

同步数据

主从节点的连接建立后,便可以开始数据同步

  • 从节点第一次/重新连接主节点,发起数据同步指令(2.8之前是SYNC,2.8之后是PSYNC)
  • 主节点接收到同步指令后,根据主从节点状态的不同,可以分为全量复制和部分复制

    全量复制

    用于初次复制或者其他无法进行部分复制的情况,将主节点的所有数据发送给从节点,是一个非常重型的操作

全量复制过程

  • 从节点判断无法进行部分复制,向主节点发送全量复制请求(或从节点发送部分复制请求,主节点判断无法进行部分复制)
  • 主节点接收到全量复制的指令后,执行bgsave, 在后台生成RDB文件,并用一个缓冲区记录记录从生成快照的时间节点后开始执行的写命令
  • 主节点bgsave执行完毕后,将RDB文件发送给从节点,从节点首先清除自己的旧数据,然后载入收到的RDB文件,将状态更新至主节点执行bgsave时候的状态
  • 主节点将缓冲区中的所有写命令发给从节点,从节点执行这些命令,从节点更新至主节点最新状态

通过全量复制的过程,可以发现全量复制是非常重型的操作

  • 主节点通过bgsave fork子进程进行RDB持久化,该过程非常耗费CPU,内存,硬盘IO
  • 主节点通过网络节点将RDB文件发送给从节点,对主从节点的带宽消耗很大
  • 从节点清空老数据、载入RDB文件的过程是阻塞的,无法响应客户端的命令

部分复制

用于网络中断后等情况的复制,只将中断期间主节点执行的写命令发送给从节点,比全量复制更加高效,但是如果网络中断时间过长,主节点没有能够完整地保存中断期间的写命令,仍然无法用部分复制还是使用全量复制

由于全量复制在主节点数据量较大的时候效率太低,redis从2.8开始提供部分复制,部分复制的实现依赖于三个重要的概念:

复制偏移量

主节点和从节点分别维护了一份复制偏移量(offset),代表的是主节点向从节点传递的字节数,主节点每次向从节点传播N个字节数据时,主节点offset增加N,从节点每次收到主节点N字节的数据时,从节点的offset增加N
offset用于判断主从节点状态是否不一致,二者offset相同,则一致,二者offset不一致,可以根据offset找出从节点缺少的那部分数据,比如主节点offset是1000,从节点offset是500,则需要把主节点501-1000的数据传递给从节点

复制积压缓冲区

主节点维护的、固定长度的、先进先出的队列,默认大小1MB,当主节点开始有从节点时创建,其作用是备份主节点最近发给从节点的数据,无论有几个从节点,复制积压缓冲区都只有一个,命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份,复制积压缓冲区还存储了每个字节对应的复制偏移量,由于复制缓冲区定长且先进先出,所以它保存的是主节点最近执行的写命令,时间较早的会被挤出缓冲区

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小判断能否进行部分复制

  • 如果从节点offset偏移量之后的数据,还在主节点复制缓冲区里面,则执行部分复制
  • 如果从节点offset偏移量之后的数据,已不在复制积压缓冲区内,则执行全量复制
服务器运行ID(runid)

每个redis节点(主从均适用)都有一个runid(启动时随机生成的,每次启动都不一样),主节点初次复制的时候,主节点将自己的runid发送给从节点,从节点保存起来,断线重连后从节点将保存的runid发送给主节点,主节点根据runid判断是否能够进行部分复制

  • 如果从节点传过来的runid和和主节点现在的runid相同,证明之前跟主节点同步过,尝试进行部分复制(具体能不能复制还要看offset和复制积压缓冲区情况)
  • 如果传过来的runid和主节点现在的runid不同,说明断线前同步的节点不是当前主节点,只能进行全量复制
psync命令的执行


图片来源《redis设计与实现》

命令传播阶段

数据同步完成后,主节点进入命令传播阶段,主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保持主从节点的数据一致性

参考文章

一文深度揭秘Redis的磁盘持久化机制

Redis 主从复制 原理与用法

redis命令参考#slaveof