Redis是运行在内存中的,这样虽然性能高,但是数据的安全却很难得到保证,一旦Redis服务出现宕机,那么存储在内存中的数据就会全部丢失,为了防止出现这样的问题,需要使用到Redis的持久化技术,即将内存中的数据保存在磁盘中,防止数据丢失
Redis有两种常见的持久化方案:分别为RDB持久化和AOF持久化
RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认保存在Redis的运行目录中。
RDB持久化在四种情况下会执行:
1)save命令
执行下面的命令,可以立即执行一次RDB:
由于Redis是单线程的,而Save命令是让主线程去执行RDB,这也就意味着在RDB过程中,其他所有的命令是会被阻塞的,直到RDB执行完,主线程才能继续接收来自用户的命令。因此Save命令只有在数据迁移时才可能会用到。
2)bgsave命令
下面的命令可以异步执行RDB:
这个命令执行后会开启一条独立进程去完成RDB的操作,而主进程可以继续处理用户请求,不会受到影响。注意这里开启的是另一个进程,而并非子线程。
3)停机时
Redis停机时会执行一次save命令,实现RDB持久化,注意这里是停机,而不是宕机
4)触发RDB条件
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有1个key被修改,则执行bgsave操作
save 900 1
save 300 10
save 60 10000
# 禁用RDB持久化
# save ""
以上配置默认是关闭的,需要我们手动开启。RDB的其它配置也可以在redis.conf文件中设置:
# 在持久化时是否对RDB文件进行压缩,建议不开启,因为磁盘空间不值钱,但是压缩操作却会消耗CPU
rdbcompression yes
# RDB文件名称,默认是dump.rdb
dbfilename dump.rdb
# RDB文件保存的路径目录,默认是在当前运行目录下
dir ./
RDB会在上面四种情况下进行持久化,但是我们仔细思考就会发现,RDB其实很难去做到在redis宕机的情况下保障数据的安全,因为宕机这种情况是很难预料的,虽然Redis内部由触发RDB的机制,但是在每一次触发之间都会有一定的时间间隔,在这个时间间隔内如果Redis服务出现宕机,那么还是会出现数据丢失的问题
当我们执行bgsave命令时,会fork主进程来得到一条子进程,子进程共享主进程的内存数据。完成fork后子进程会读取内存数据并写入 RDB 文件。将内存数据写入RDB这个过程是异步的,但是fork主进程得到子进程的过程却是阻塞的,在fork过程中,主线程只能做这一件事,而不能去接受用户请求。
那fork是什么呢?
首先我们需要了解,Redis 主进程是在内存中实现对数据的读写的,但是在 Linux 系统中,所有的进程都无法直接操作物理内存,而是只能操作Linux系统给每个进程分配一个虚拟内存,同时操作系统会维护一个虚拟内存与物理内存之间的映射表,这个表被称之为页表。主进程操作虚拟内存,而虚拟内存基于页表的映射关系到物理内存,这样就可以实现对物理内存的读写。
而所谓的fork,在Linux中是一个系统调用的函数,这个函数会通过一个已经存在的进程来创建一个与原进程几乎完全相同的进程,在这里原进程被称为父进程或主进程,新创建的进程也被称为子进程,这两个进程可以做完全相同的事。
也就是说,当我们在执行bgsave命令时,实际上是创建了另一个与原来的Redis进程完全相同的子进程,然后让这个子进程去执行类似于Save命令的操作。
在fork的过程中,会将主进程的页表拷贝一份给子进程,而由于页表是虚拟内存与物理内存之间的映射关系,子进程拿到主进程的页表之后,也就有了与主进程相同的映射关系,当子进程在操作自己的虚拟内存时,因为映射关系与主进程相同,最终就会映射到相同的物理内存,这样也就实现了子进程与主进程之间的内存空间共享,到这里,异步持久化也就实现了
因为无需拷贝内存中的数据,而是只需要通过拷贝页表来实现内存共享,所以fork的速度非常快,所以虽然会阻塞进程,但是阻塞的时间是很短的。fork完成之后,子进程会读取共享内存中的数据并将这些数据写入到磁盘当中,然后替换旧的 rdb 文件。
这里还有一个问题,就是子进程在写rdb文件的过程中,主进程是可以接收用户请求并修改内存中的数据的,如果主进程在修改数据的同时子进程也在写数据,那么读与写之间就可能会发生冲突,进而产生脏数据。为了避免这种情况的发生,fork 的底层采用了一种copy-on-write 的技术:
上述技术虽然能解决读写冲突的问题,但是由于需要拷贝数据副本,因此会带来额外的内存消耗,一旦子进程的写操作执行时间过长,主线程收到过多的修改请求,那么对内存的负担就会大大增加,很有可能出现内存溢出的风险,为了解决这个问题,一般部署redis的服务器并不会将所有的内存都分配给Redis进程,而是会预留一些内存空间,以免在进行rdb操作时内存不够用。
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
相信对mysql有所了解的朋友,看完上面的话之后,就会发现aof与mysql中的redologo是比较相似的。只要我们将redis处理过的每一条命令都记录下来,这样在Redis出现故障之后,就可以通过重新运行这些命令来恢复到故障之前的状态
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"
AOF的命令记录的频率也可以通过redis.conf文件来进行配置:
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
三种策略对比:
因为AOF文件中记录的是一条一条命令,所以AOF文件会比RDB文件要大的多。而且AOF会将针对同一个key的多次写操作全部记录下来,实际上如果我们多次修改一个Key的值,那么只有最后一次修改才是有意义的,而其他多余的写操作不仅会占用磁盘空间,而且会在我们做数据恢复时严重拖慢速度,那么有没有什么办法可以解决这个问题呢?
Redis为我们提供了一个bgrewriteaof命令,通过这个命令可以让AOF文件进行重写,用最少的命令达到相同效果。
如上图,AOF文件中原本有三个命令,但是set num 123 和 set num 666
都是对num的操作,第二次set会覆盖第一次set的值,因此第一个命令记录下来是没有什么意义的。所以当我们执行重写命令后,AOF文件的内容就会变成:mset name jack num 666
需要注意的是,执行bgrewriteaof命令进行重写的过程也是异步的,并不会阻塞主进程。同时,Redis也会在触发阈值时自动去重写AOF文件,阈值也可以在redis.conf中配置:
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用,两者同时使用时,在 Redis 重启后,会以 AOF 优先来做恢复。