Redis原理之_未雨绸缪_持久化
前言
Github:https://github.com/HealerJean
1、引入
问题1:Redis为什么要有持久化?
答案:如下
Redis 的数据全部在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制 来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。 持久化功能有效避免进程退出造成的数据丢失问题。下次重启的时候利用之前持久化的文件即可实现数据恢复。
问题2:Redis 的持久化机制有两种,哪俩种?有什么区别?
答案:如下(包含下图)
1、快照:快照是内存数据的二进制序列化形式,在存储上非常紧,快照是一次全量备份
2、 AOF 日志,AOF 日志是连续的增量备份,AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会 变的无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长。 所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身

2、RDB
RDB持久化是将当前进程数据生成快照保存到硬盘的过程
RDB持久化的过程分为手动触发和自动触发
1.1、快照原理
问题1:Redis是单线程程序在RDB快照持久化方面有什么困难?
答案: Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。在服务线上请求的同时,Redis 还需要进行内存快照,内存快照要求 Redis 必须进行文件 IO 操作,可文件 IO 操作是不能使用多路复用 API。
问题2:除了不能使用多路复用还有什么困难因素呢?
答案:RDB快照这意味着单线程同时在服务线上的请求还要进行文件 IO 操作,文件 IO 操作会严重拖垮服务器请求的性能。还有个重要的问题是为了不阻塞线上的业务,就需要边持久化边响应客户端请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它给删掉了,还没持久化完呢,这尼玛要怎么搞?
问题3:怎么解决上面提到的Hash字段正在持久化,还没持久化完成,就给删掉的问题呢?
答案:Redis 使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化,这个机制 很有意思,也很少人知道。多进程 COW 也是鉴定程序员知识广度的一个重要指标。
问题4:如果上面提到的Hash在RDS快照持久化之后删掉了,那是丢数据了吗?
答案:是的,因为RDS是快照,只能持久化那一时刻的数据,如果子进程持久化中,父进程将数据修改了,子进程是感知不到的,这个时间宕机了,会存在脏的Hash数据。同样,如果是父进程添加缓存,如果宕机了,也会出现RDS快照文件缓存没有持久化导致丢失数据。所以,可以很明显的看到,RDB有它的不足,就是一旦出现问题,那么我们的RDB文件中保存的数据并不是全新的。
1.2.1、fork(多进程)
Redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以将父子进程想像成一个连体婴儿,共享身体。这是
Linux操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的一瞬间,内存的增长几乎没有明显变化。
问题1:描述一下这种fork操作?
答案:用 Python 语言描述进程分离的逻辑如下。fork 函数会在父子进程同时返回,在父进程 里返回子进程的 pid,在子进程里返回零。如果操作系统内存资源不足,pid 就会是负数,表 示 fork 失败。
pid = os.fork()
if pid > 0:
handle_client_requests() # 父进程继续处理客户端请求
if pid == 0:
handle_snapshot_write() # 子进程处理快照写磁盘
if pid < 0:
# fork error
问题2:父子进程怎么分工工作的的?
答案:子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成 当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的, 还是进程产生时那一瞬间的数据,子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安 心的遍历数据了进行序列化写磁盘了。
问题3:父进程一直复制内存,内存会一直增长吗?
答案:随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例 往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页 面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。

1.1.3、bgsave 命令流程
1、执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
2、父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。
3、父进程fork完成后,bgsave命令返,“Background saving started信息并不再阻塞父进程,可以继续响应其他命令
4、子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统计的rdb_last_save_time选项。
127.0.0.1:6379> lastsave
(integer) 1523876908
127.0.0.1:6379>
5、进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项

1.2、触发机制
1.2.1、手动触发
1.2.1.1、save命令(已经被丢弃)
save命令,阻塞当前的Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞。线上环境不建议使用,save命令已经被丢弃。运行
save命令Redis日志如下:
DB saved on disk
1.2.1.2、bgsave命令
Redis进程执行fork操作创建子进程,RDB持久化的过程由子进程负责,完成后自动结束,阻塞只发送在fork阶段,一般时间很短。因此Redis内部所有涉及RDB的操作都是bgsave命令的方式,save命令已经被丢弃。
Background saving started
1.2.2、自动触发时机
1.2.2.1、执行shutdown命令的时候
执行
shutdown命令的时候,如果没有开启AOF持久化,则自动执行bgsave
1.2.2.2、执行debug relod
执行
debug relod命令的时候也会自动触发save操作
1.2.2.3、从节点执行全量复制时
如果从节点执行全量复制操作,主节点自动执行
bgsave生成RDB文件并发送给从节点
1.2.2.4、配置自动触发
自动触发
bgsave,使用save相关配置,如“save m n”,表示m秒内数据集存在n次修改时,
#Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
1.3、RDB文件存储位置
RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行
config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
127.0.0.1:6379> config set dir /usr/local/redis-4.0.8/myrdb
OK
127.0.0.1:6379> config set dbfilename myrdb.rdb
OK
127.0.0.1:6379> bgsave
Background saving started
127.0.0.1:6379>
1.4、RDB的优缺点
优点:
1、RDB是一个紧凑压缩的二进制文件,代表某个时间点的数据快照,远远小于内存大小,非常适用于备份,全量复制等场景,比如每6个小时执行bgsave备份,并把RDB文件拷贝到远程机器等位置,用于灾难恢复
2、Redis加载RDB恢复数据比AOF快多了
缺点:
1、RDB无法做到实时持久化/秒级持久化(无法做到秒级,这样一旦宕机,在持久化期间数据的修改就会丢失)
2、每次bgsave都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高
3、AOF
3.1、AOF原理
问题1:有了RDB为什么会出现AOF?
答案:AOF(append only file)持久化,AOF主要就是解决数据持久化的实时性,RDB快照无法做到秒级持久化。
问题2:AOF是如何实现秒级持久化的?
答案: AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的 指令记录。重启时再重新执行AOF文件中的命令达到数据恢复的目的
问题3:AOF怎么在重启的时候恢复数据?
答案:假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过 对一个空的 Redis 实例顺序执行所有的指令,也就是「重放」,来恢复 Redis 当前实例的内 存数据结构的状态。
问题4:AOF是如何保存命令和恢复数据的?
答案:Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。
问题5:一直往AOF的日志文件中追加指令,内存变得很大了怎么处理?
答案:Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦 身。
3.2、开启AOF功能(默认是关闭)
开启
AOF功能需要进行配置,默认是不开启的,appendonly yes,观察redis.conf文件,默认文件名appendonly.aof
# 指定 AOF 文件名
appendfilename appendonly.aof
#修改为守护模式
daemonize yes
#设置进程锁文件
pidfile /usr/local/redis-4.0.8/redis.pid
#端口
port 6379
#客户端超时时间
timeout 300
#日志级别
loglevel debug
#日志文件位置
logfile /usr/local/redis-4.0.8/log-redis.log
#设置数据库的数量,默认数据库为0,可以使用SELECT <dbid>命令在连接上指定数据库id
databases 16
##指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合
#save <seconds> <changes>
#Redis默认配置文件中提供了三个条件:
save 900 1
save 300 10
save 60 10000
#指定存储至本地数据库时是否压缩数据,默认为yes,Redis采用LZF压缩,如果为了节省CPU时间,
#可以关闭该#选项,但会导致数据库文件变的巨大
rdbcompression yes
#指定本地数据库文件名
dbfilename dump.rdb
#指定本地数据库路径
dir /usr/local/redis-4.0.8/db/
#指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能
#会在断电时导致一段时间内的数据丢失。因为 redis本身同步数据文件是按上面save条件来同步的,所以有
#的数据会在一段时间内只存在于内存中
appendonly no
#指定更新日志条件,共有3个可选值:
#no:表示等操作系统进行数据缓存同步到磁盘(快)
#always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全)
#everysec:表示每秒同步一次(折衷,默认值)
appendfsync everysec
3.2.1、命令写入AOF日志流程
1、所有写命令会被追加aof_buf(缓冲区)中
2、AOF缓冲区根据对应的策略向硬盘做同步操作
3、随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
4、当redis服务器进行重启时,可以加载AOF文件进行数据恢复
3.2.2、为什么要用文本协议格式
AOF命令写入的内容直接是文本协议格式,例如
set hello world这条命令,在AOF缓存中会追加如下文本
1、文本协议有很好的兼容性。可读性,,方便直接修改和处理
2、开启AOF后,所有写入命令都包含追加操作,直接采用文本协议格式,避免了字符转换带来的二次开销。
3.2.3、AOF为什么会把命令放到aoc_buf缓存中
1、Redis使用单线程响应命令,如果每次AOF文件命令都直接追加到硬盘,也就是说对文件进行IO操作,而对文件IO操作不支持多路复用的,文件 IO 操作会严重拖垮服务器请求的性能
2、Redis提供了多种缓冲区到硬盘的同步的策略。在性能和安全性方面做出平衡
3.3、AOF 重写机制
随着
AOF文件越来越大Redis引入AOF重写机制,压缩文件体积。AOF文件重写就是讲Redis进程内的数据,转化为写命令同步到新的AOF文件的过程
3.2.1、瘦身原理:
Redis提供了bgrewriteaof指令用于对AOF日志进行瘦身。其原理就是1、开辟一个子进程对内存进行遍历转换成一系列
Redis的操作指令,序列化到一个新的AOF日志文件中(个人感觉有点类似于快照)。2、序列化完毕后再将操作期间发生的增量
AOF日志追加到这个新的AOF日志文件中3、追加 完毕后就立即替代旧的
AOF日志文件了,瘦身工作就完成了。
3.3.2、重写后的AOF文件为什么可以变小
1、进程内已经超时的数据不再写入文件
2、旧的AOF文件含有无效命令,如del key1,hdel key2等,重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令,不会包含这些无用的命令。
3、多条写命令可以合并成一个,如:lpush list a,lpush list b ,转化为 lpush list a b,同时防止单挑命令多大造成客户端缓冲区溢出。
1.3.3、AOF重写过程的触发
1.3.3.1、手动触发:bgerwriteaof 命令
直接调用
bgerwriteaof命令,不管有没有开启AOF都会执行
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
127.0.0.1:6379>
1.3.3.2、自动触发:达到阈值
设置参数值 服务器在
AOF功能开启的情况下,会维持以下三个变量:1、记录最后一次
AOF重写之后,AOF文件大小的变量aof_rewrite_base_size。2、记录当前
AOF文件大小的变量aof_current_size。3、增长百分比变量
aof_rewrite_perc。
以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
1、没有bgsave命令(RDB持久化)/AOF持久化在执行;
2、没有bgrewriteaof命令在进行;
3、当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小。当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为 100%)
3.4、fsync
AOF日志是以文件的形式存在的,当程序对AOF日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将数据刷回到磁盘 的。
问题1:这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个 时候就会出现日志丢失。那该怎么办?
答案:Linux 的 glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。
问题2:真的要实时调用fsync,会有什么问题吗?
答案:是 fsync 是一个 磁盘 IO 操作,它很慢!,AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系
统 IO 负担。如果 Redis 执行一条指令就要 fsync 一次,那么 Redis 高性能的 地位就不保了。
问题3:你说的要实时,现在又不能每次执行执行就调用一次,那该怎么调用fsync这个函数呢?
答案:生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失,是建议的同步策略,也是默认配置,做到监控性能,理论上只有在系统死机的情况下丢失1秒的数据。
问题4:除了fsync,还有其他策略吗?
答案:Redis 同样也提供了另外两种策略
1、永不 fsync——让操作系统来决定合适同步磁盘,很不安全
2、来一个指令就 fsync 一次——非常慢。但是在生产环境基本不会使 用,了解一下即可。
3.4.1、缓冲区到硬盘的同步的策略
Redis提供了AOF多种缓冲区同步文件策略,由参数appendfsync控制,不同值的含义如下
| 可配置值 | 说明 |
|---|---|
always: |
来一个指令就 fsync 一次(慢,安全) |
everysec: |
每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置(折衷,默认值) |
no: |
永不 fsync——让操作系统来决定合适同步磁盘(快,不安全) |
4、重启选择RDB文件还是AOF文件
AOF和RDB文件都可以用于服务器重启时的数据恢复
1、AOF 持久化开启且存在AOF文件的时,优先加载AOF文件,打印如下日志:
* DB loaded from append only file : 5.841 seconds
2、AOF 关闭或者 AOF持久化虽然开启但是AOF文件不存在的时候,加载RDB文件,打印如下日志
* DB loaded from disk : 5586 seconds
3、加载 AOF或者RDB成功之后,Redis启动成功,如果AOF或者RDB文件存在错误,则Redis启动失败并报错
5、运维
快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。 运维难度
1、遍历整个内存,大块写磁盘会加重系统负载
2、
AOF的fsync是一个耗时的IO操作,它会降低Redis性能,同时也会增加系统IO负担
1、持久化在从节点执行:通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。
2、做好实时监控和多加几个从节点,防止从节点故障:如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别是在网络分区出现的情况下又不小心主节点宕机了,那么数据就会丢失,所以在生产环境要做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失
6、Redis 4.0 混合持久化
问题1:为什么用混合的持久化呢?
答案:之前的版本中,重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常 使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis实 例很大的情况下,启动需要花费很长的时间。
问题2:Redis 4.0 是怎么解决的?
答案:Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文 件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 RDB持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。



