对于Mysql,数据是持久化在磁盘上的。如果误删数据,可以使用binlog进行恢复;突然宕机时,其本身可以借助redo log进行崩溃恢复。
更多关于Mysql日志的内容,可以参考我的另外一篇文章数据库日志——binlog、redo log、undo log扫盲
而对于Redis,一般是把数据直接存储在内存中。如果不做任何持久化工作,在出现宕机后,内存中的全部数据就会丢失。
显然,业务方是不能容忍这样的情况发生的。好在Redis提供了一系列的持久化机制,分别是AOF日志与RDB快照。
AOF全称是Append Only File,Redis每次执行完一个写类型的语句后,会将该语句以某种格式使用追加的方式顺序写入AOF日志中。
值得注意的是,AOF是默认不开启的。
以winows为例,进入到redis安装目录中的redis.windows.conf中,将appendonly的值修改为yes,即可开启AOF
# 默认关闭
appendonly yes
# AOF的默认文件名称
appendfilename "appendonly.aof"
当执行以下命令后
set java helloworld
在appendonly.aof文件中,可以看到以下内容
*3 代表当前命令有3个部分
$3 第1部分命令的长度,3个字符
set 第1部分命令
$4 第2部分命令的长度,4个字符
java 第2部分命令
$10 第3部分命令的长度,10个字符
helloworld 第3部分命令
当我们首次使用某个客户端执行命令时,客户端会自动帮我们补充select 0(即选择编号0的数据库),这个命令也会被保存在AOF日志中。
大致的流程如图所示
在server中,主线程执行完命令之后,会立即将命令写入AOF缓冲中。之后会调用系统函数write(),将命令写入内核缓冲区,并返回给客户端成功的响应。
内核会在合适的时机将内核缓冲区的中的数据写入到磁盘中。
我们设想其中某个阶段宕机时,会不会产生不一致的情况:
1、如果命令执行成功,但写入AOF缓存前崩溃重启,客户端会收到执行失败或超时的响应。重启之后AOF文件中没有该条数据,这个时候,数据是一致的。
2、如果命令执行成功,写入AOF缓存成功,但调用write时崩溃重启。其实这种情况和第一条一样,恢复后数据还是一致的。
3、如果命令执行、写入AOF缓存与内核缓存都成功,客户端会收到成功的响应。如果这个时候机器宕机,内核缓冲区中的数据将会丢失,也就是最后的AOF文件缺少该条命令,恢复后,就会产生数据不一致的情况。
第3种情况发生时,就会出现数据不一致的后果。怎么处理呢,很简单啊,变异步为同步不就行了吗。
调用write写入内核缓冲区后,再调用fsync强制让内核缓冲区中的数据刷到磁盘上,刷盘成功后,再返回给客户端响应。
这样的解决方式看似可以,但是刷盘的操作非常耗时。在Redis执行大量命令的时候,会一直进行不断的刷盘,当磁盘压力过大时,会阻塞下一个命令的执行,大大降低性能。
看来得把握刷盘的时机,刷得慢了,机器崩溃恢复后就会丢失大量数据。刷得快了,就会严重降低性能。
不过,Redis本身也提供了3种写回策略。
这3种策略体现了不同的刷盘频率,因此拥有不同级别的一致性与性能。
always策略最大程度上保证数据不丢失,但性能最差。
no策略性能最好,但在机器崩溃重启后会丢失比较多的数据。
everysec是一种折中的策略,较always有不错的性能。在极端的情况下,只会丢失1秒内的数据,是比较推荐的方式。
redis.windows.conf中有appendfsync配置项,用来配置写回策略,默认的策略是everysec 。
随着Redis不断记录AOF日志,AOF日志文件将变得越来越大,用作恢复的时间也将越长。因此需要一种方式减少文件的大小,这时候AOF重写就派上用场了。
在出现触发重写的条件时(例如AOF文件达到某个阈值),Redis扫描整个库的所有数据,将数据以命令的方式记录在新的AOF日志中,待记录完成后,使用新的AOF日志替换旧的即可。
旧日志中,可能存有对同一个key的多次操作命令,重写的目的就是取最后一次有效的命令,删除那些历史命令,从而达到瘦身、压缩的效果。
刚才提到,AOF重写会扫描整个库的数据,因此注定就是一个非常耗时的操作,那么就不会在主线程中做,而是通过主线程fork出一个子进程进行重写的。
重写的流程图如下:
1、当AOF日志文件的大小超过执行的阈值后,就会触发AOF重写
2、主线程fork出一个子进程,fork的过程仍然是阻塞的。fork完之后,主线程依然可以接受命令并处理
3、子进程与主线程共享一个实例的所有数据,子进程会对整个实例进行扫描,将其中的数据以命令的格式写入到重写日志中。
4、在子进程重写的过程中,主线程可以接受命令,假设这个时候执行了一条写命令。
5、主线程会将数据存入到库中,利用写时复制技术,子进程不会感知到数据有任何变化。
6、主线程将日志先写入AOF缓冲区,再写入重写缓冲区。
7、由特定写回策略,将缓冲区中的数据写入到旧的AOF日志中。
8、当子进程结束扫描,并且将所有命令写入重写日志后,再将重写缓冲区中的数据追加到重写日志中。
9、最后一步,主线程感知到子进程重写日志完成,于是使用新的日志文件替换旧的文件。
也许有人会发出以下的疑问
如果是创建出来一个子线程,那么主线程在写入,子线程在读取,是需要通过加锁的方式来保证线程安全的,加锁就意味着降低性能。
而如果是fork出来子进程,主线程和子进程同样需要共享数据,当主线程写入数据的时候,会利用写时复制技术,避免加锁。
大家应该都知道三角函数吧,嗯,这和写时复制没什么关系。
CopyOnWriteArrayList就利用到了写时复制,读不加锁,写则是复制一份数组出来,在新的数组上进行修改,最后替换引用。非常适合应用于读多写少的场景,缺点是在替换引用前,线程读到的是旧数据。
主线程在fork出一个子进程的时候,会将自己的页表(虚拟地址与物理地址的映射表)复制一份出来给子进程,而不是直接复制内存。否则在重写的时候,Redis占用内存会立即翻倍。
这样的话,子进程就可以随意访问主线程中的数据。而当主线程修改一些实例数据时,就会复制一份物理内存出来,并变动主线程的页表,在新的内存地址上存储写之后的数据。因为没有变动子进程的页表,因此主线程写入的数据对子进程不可见。
CopyOnWriteArrayList的缺点在于读到的可能是旧数据,子进程在扫描的时候,其实扫描到的也是旧数据,因此需要在重写结束后做补偿。
子进程在重写的过程中,扫描的数据是fork动作结束的那一刻的快照。而在重写的过程中,主线程依然可以执行命令,那么这些多出来的写命令就可以放在一个独立的重写缓冲区中。在重写完成后,再将重写缓冲区中的内容追加到重写日志中,这就保证了数据的一致。
尽管存在AOF重写机制,但重写后的日志文件还是大,恢复速度较慢。
有没有一种直接存储数据,而不是存储命令(命令的大小显然大于数据本身)的方式呢?RDB就闪亮登场了!
RDB的全称是Redis Database Backup,即数据备份。
会将某一时刻内的所有数据生成一个快照文件。该文件是一种经过压缩的二进制文件,默认名称为dump.rdb,可通过修改dbfilename参数来改变RDB文件名。
快照文件仅保存数据,不保存额外的操作命令,且经过压缩,因此在恢复速度上快于AOF。但RDB没法做到实时的持久化,而AOF可以基本做到。
直接在主线程中执行,会阻塞其他命令
主线程fork出来一个子进程,由子进程去执行备份。
整个fork的过程,是会阻塞主线程的。由于不会复制物理内存,因此fork是快速的。
fork结束后,主线程依然可以执行其他的命令。
redis.windows.conf中有如下的几个配置可用于触发生成RDB文件
# 900秒内至少出现1条写命令就触发
save 900 1
# 300秒内至少出现10条写命令就触发
save 300 10
# 60秒内至少出现10000条写命令就触发
save 60 10000
这种方式,也是通过fork出一个子进程来做的。
客户端使用bgsave命令时,主线程fork出来子进程,由子进程完成备份。
在子进程备份期间,主线程依然可以执行命令。但该条数据并不会被子进程扫描到,和AOF重写一样,都利用到了写时复制。
既然RDB文件占用小,恢复速度快,那可以大幅增加RDB生成的频率吗?
那显然是不可以的,有可能上一轮RDB还未生成,下一轮又开始了。而且也存在性能问题,save全程都会阻塞主线程,bgsave的fork操作同样也会阻塞主线程。
当然,RDB这种方式,如果在持久化的过程中发生宕机,会丢失在上次备份之后产生的所有数据。
下面使用一张表格来直观地展示两者之间的优缺点
AOF | RDB | |
---|---|---|
持久方式 | 实时追加写入 | 某一时刻的数据快照 |
文件大小 | 包含操作命令,文件较大 | 只包含数据,且经过压缩,文件较小 |
恢复速度 | 慢 | 快 |
数据可靠性 | 由写回策略决定,最好情况下仅丢失1秒数据 | 由触发频率决定,会丢失上一次备份之后产生的所有数据,可靠性不如AOF |
另外值得注意的是,当同时开启AOF与RDB时,Redis会优先使用AOF日志来恢复数据。
RDB相比而言,会丢失较多的数据。AOF只有在实例数据比较大的时候,恢复速度才慢。
既然AOF与RDB独有各自的优势,能否结合二者的特点呢?
在Redis4.0中,出现了一个新的模式——混合持久化。具体来讲,就是全量RDB+增量AOF,将两种类型的日志文件存放在一起。
RDB可以以较低的频率执行,两次RDB之间的产生的增量数据记录在AOF日志中,因此增量AOF日志的文件很小。
因此Redis在恢复时,先加载RDB数据,再重放增量的AOF日志。不需要像之前重放全量AOF日志,因此恢复效率大大提升。