我们经常在数据库层上加一层缓存(如Redis),来保证数据的访问效率。
这样性能确实也有了大幅度的提升,但是本身Redis也是一层服务,也存在宕机、故障的可能性。
一旦服务挂起,可能生产的后果包括如下几方面:
1、Redis的数据是存在内存中的,所以一旦挂起,内存中的数据会全部丢失。
2、I/O从内存层级迁移到磁盘层级,性能极速下降。
3、原本访问缓存的请求会透过缓存层直接投向数据库,给数据库带来极大的压力,甚至导致雪崩。
所以,缓存层崩溃产生的后果是灾难的。为了避免宕机和宕机后的数据丢失, 为了保证数据的快速恢复,Redis提供了两个持久化数据的能力, AOF(Append Only FIle)日志 和 RDB 快照。
大规模高并发的分布式场景,经常会遇到问题就是Redis挂起,导致访问失败,而所有的请求透过缓存层投向数据库,给数据库造成极大的压力。
而Redis的数据是存储在高速缓存中,即使我们重启并且恢复使用,缓存池依旧是空的,因为内存被释放了。
重新建立缓存的过程,对数据库也是一个暴击的过程,很可能会导致整个系统调用链的雪崩。
我们知道,Redis 数据都是保存在内存中,能不能将内存中的数据进一步写到磁盘上,Redis 重启的时候就可以把磁盘上的数据快速恢复到内存中。这样,即使Redis宕机重启之后,依然能够正常的提供服务。
但是不能忽略一个问题,Redis和MySQL最大的区别之一就是一个存储在内存,一个持久化在磁盘。但是如果每次数据的变化(新增、修改、删除缓存)都要写内存并同时写磁盘,这样成本太高,内存+磁盘,会让 Redis 性能大大降低。而且还要保证原子性操作,避免内存和磁盘的数据不一致。
为了避免实时写入高频操作磁盘带来的负面效应。Redis提供了内存快照策略。
我们知道,Redis 在 执行写(增、删、改)指令过程中,内存中数据会持续的在变化。而内存快照,指的是 Redis 内存中的数据在某一刻的状态。就好比如是拍照一样,你把那一刻的数据都定格下来,持久化到磁盘上。打游戏的同学可以想象存盘。
快照文件我们称之为 RDB 文件,即 Redis DataBase 的缩写。
Redis 通过定时执行 RDB 内存快照,这样就不必每次执行写指令都存盘,只需要在执行内存快照的时候写磁盘。这样既保证Redis的高效读写,还实现了定时持久化,宕机后可快速恢复数据。
如上图,在做数据恢复时,直接将 RDB 文件读入内存完成恢复。
Redis 提供了两种模式来生成 RDB 文件:
save模式是主进程执行,非常不建议使用主进程执行的方式,在 《 深刻理解高性能Redis的本质 》 中,
我们知道他的主操作都是在单线程模型上完成的。所以尽量避免 RDB 文件生成影响主线程的网络I/O和键值对读写。
上面提到的另外一种方式,fork一个子进程来写RDB文件。
Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化,这个很重要,具体可以了解下这篇《 Copy On Write机制 》,写的不错。
Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,由这个子进程来处理快照持久化的动作,子进程可以共享主进程的所有内存数据,所以它读取到主进程的数据之后写入到 RDB 文件。而父进程继续处理客户端的写操作,不受影响。
在创建 RDB 文件时,程序会对数据库中的键进行检查,仅仅将未过期的键保存到新创建的 RDB 文件中。
当主进程执行写指令修改数据的时候,这个数据就会复制一份副本, bgsave 子进程读取这个副本数据写到 RDB 文件,所以主进程就可以直接修改原来的数据。
这既保证了快照的完整性,也允许主进程同时对数据进行修改,避免了对正常业务的影响。
虽然说Redis 使用 bgsave 函数 fork 子进程在后台完成 内存中的数据做快照,没有影响父进程继续处理客户端的各种操作。
但是需注意一点,过于频繁的执行全量的数据快照,必然会导致严重的性能开销:
AOF 日志存储了 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。
假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令。
也就是说,可以通过重放(replay),来建立 Redis 当前实例的内存数据结构。这种模式有没有很熟悉,有没有想到MySQL主从同步时候的relay log。
AOF记录日志有两种模式,一种是预写式日志,也称写前日志(Write Ahead Log, WAL): 在实际写数据之前,将修改的数据写到日志文件中。
另外一种是写后日志: 先执行写操作,当数据存入内存后,再记录日志。
预写式日志类似 MySQL Innodb 引擎 中的 redo log,修改数据前先记录日志,再修改。
Redis 接收到 set keyName someValue
命令的时候,会先将数据写到内存,Redis 会按照如下格式写入 AOF 文件。
*3
:表示当前指令分为三个部分,每个部分都是 $ + 数字
开头,后面是3部分的具体内容:指令、键、值。
数字
:表示这部分的命令、键、值多占用的字节大小。比如 $3
表示这部分包含 3 个字符,也就是 set
的长度。
推荐使用写后日志的模式,避免了额外的检查开销,不需要对执行的命令进行语法检查。如果使用写前日志的话,就需要先检查语法是否有误,否则日志记录了错误的命令,在使用日志恢复的时候就会出错。另外,写后才记录日志,不会阻塞当前的 写 指令执行。
# set keyName someValue *3 $3 set $7 #长度为7 keyName $9 #长度为9 someValue # 执行 mset key1 1 ,key2 2 ,key33 3 # aof日志如下: *7 # 本批命令需要往下读7行非 $ 开始的命令 $4 #接着读取4个字节宽度,‘mset’长度为4,记为 $4 mset $4 #接着读取4个字节宽度,‘key1’长度为4,记为 $4 key1 $1 #接着读取1个字节宽度,‘1’长度为1,记为 $1 1 $4 key2 $1 2 $5 #接着读取的字节宽度,‘$key33’长度为5,记为 $5 key33 $1 3
上面的问题,在Redis高频读写的时候是必然存在的,想要解决,在写入的时候做一层缓冲就可以了,避免直塞。这时候Redis提供了一种执行策略叫写回策略。
为了提高日志文件的写入效率,写回策略会做如下变化:
fsync
和 fdatasync
两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。appendfsync
写回策略直接决定 AOF 持久化功能的效率和安全性,以下是 appendfsync
的3个枚举:写磁盘会带来性能上的损耗,所以写回的策略要根据实际情况做一个取舍,比如你是偏向性能还是可靠性。
always同步写回可以做到数据不丢失,但是每次执行写指令都需要写入磁盘,性能最差。
everysec每秒写回,避免了同步写回的性能开销,但是如果服务发生宕机,会有大约1s时间周期的数据丢失,这种模式是在性能和可靠性之间做了妥协。
no操作系统控制,执行写指令后就写入 AOF 文件缓冲,再执行后续的写磁盘指令,性能最好,但有可能丢失更多的数据。
我们可以根据服务的实际情况来抉择策略,看是偏向高性能还是高可靠。
现实情况下,无论使用RDB或者AOF都差点意思。使用 rdb 来恢复内存状态,势必会丢失一部分数据。 使用 AOF 日志重放,重放对性能有一定的影响,而且在 Redis 实例很大的情况下,需要花费很长的时间。
Redis 4.0 解决了这个问题,才用了一个新的持久化模式——混合持久化,该 混合模式 默认是关闭状态的。
将 RDB 文件的内容和 rdb快照时间点之后的增量的 AOF 日志文件存在一起。这时候 AOF 日志不需要再是全量的日志,而是最近一次快照时间点之后到当下发生的增量 AOF 日志,通常这部分 AOF 日志很小。
所以执行有如下顺序: