文章目录
- 前言
- RDB
- 快照原理
- 保存时机
- AOF
- 同步策略
- AOF重写
- 混合持久化
- 总结
前言
redis作为内存数据库,数据都在内存里,如果突然宕机,则数据都会丢失(这里假设不使用非易失性内存),redis提供了持久化机制来防止这种情况发生
如果将redis仅作为缓存使用,且不需要宕机后快速生成缓存数据,可以不使用持久化机制,还能提升性能
redis提供了以下两种持久化方式:
- RDB(redis database):定时将某一时刻内存中的数据保存到磁盘上
- AOF(append only file):通过持续增量记录redis的写命令来持久化数据
下面介绍其原理和实现
RDB
快照原理
redis通过定时将内存中某一时刻的快照持久化到磁盘上,来实现数据保存
redis怎么“捕获”某一时刻的快照?
一个简单的想法是,在持久化的过程中,不处理任何请求,也就是执行save
命令
但这样会阻塞线上业务,更好的做法是:边持久化边响应客户请求,也就是bgsave
命令
以快照的方式持久化,需要保证获取的是“一致性”
的快照
什么意思呢?在持久化的过程中,如果先持久化了内存的前半部分,再持久化后半部分,可能在持久化后半部分时,又有请求同时写了前,后半部分的数据。而此时前半部分已经持久化完毕,不会修改了。这样前后两部分的数据就是“不一致”
的
举个例子:
- 内存中一开始A=1,B=2
- 将A=1序列化到磁盘
- 某请求在内存中将A,b修改为2,此时磁盘上A=1
- 继续序列化,将B=2写入磁盘
- 出现问题,此时磁盘中是不合法的数据,因为内存中从来没有某一时刻是A=1,B=2,也就是说
A的老版本
和B的新版本
混在一起了
那怎么获取一致性的快照
呢?
- Mysql Innodb的可重复读隔离级别采用MVCC的方式,使得每次的读操作获取的都是一致性的数据,同时不阻塞其他读写请求
- redis采用多进程
写时复制
(copy on write)技术来实现
当redis服务端收到bgsave命令时,调用fork函数
产生一个子进程,由该子进程创建RDB文件,主进程继续响应客户请求
刚创建子进程时,其和父进程共享数据段,这是操作系统节约内存的机制
只有在某进程需要对数据进行修改时,才会将其复制一份,单独给自己使用,才这个复制出来的页面进行修改。也就是说,子进程在生成快照的过程中需要遍历的页面,是不会被修改
的,永远是刚fork出来的样子。
刚fork出来的页面肯定是那一时刻的一个一致性快照,因此将其序列化到磁盘没有任何问题
保存时机
redis在什么时机会触发一次fork子进程生成快照文件的操作呢?
若在配置文件中进行如下save配置:
save 900 1
save 300 10
save 60 3600
以上配置表示redis会在以下情况满足时,自动执行bgsave命令:
900秒
内至少1个key
发送变化(新增,修改,删除)300秒
内至少10个key
发送变化60秒
内至少3600个key
发送变化
为什么需要配置多个规则?
试想如果只有中间的规则(300秒内至少10个key发送变化)
那如果redis中只有9个key发生变化,则不管经过多久,这9个key都不会被持久化,因为永远不满足
300秒内至少10个key发送变化的条件
因此在save配置项中最好有一条 save XXX 1 的配置兜底
,确保redis中所有的变动在一定时间内都能被持久化
如果短时间内变化的次数较多,但根据唯一的那条配置,需间隔300秒后才会进行一次持久化。如果宕机,则会丢失从上一次持久化到宕机这段时间内的修改。也就是说最多会丢失300秒的数据
因此在save配置项中最好有一条 save XXX(小于300秒) XXX(大于10个)的配置,这样在短时间内变更较多时,能提早
,更频繁
地持久化。如果宕机,最多只会丢失更短时间间隔的数据
怎么实现的?
以下用go代码示例,不过没有特殊的语法,不影响理解
redis维护了两个变量:
dirty
:在上次bgsave后,执行了多少次修改操作lastsave
:上次bgsave的时间
以及配置的规则saveConfig:
type saveConfig struct {
Change int // 多少时间内 save 60 3600 中的 3600
time int // 多少个key发生变化 save 60 3600 中的 60
}
在每次事件循环中尝试每个配置,如果符合某个配置的条件,执行bgsave
func serverCron(){
// 执行其他操作
interval := time.Now().Sub(lastsave)
// 尝试每个配置
for saveConfig := range saveConfigs {
// 如果符合某个配置的条件,执行bgsave
if dirty >= saveConfig.Change && interval >= saveConfig.Time {
bgsave()
break
}
}
}
AOF
与RDB通过快照的方式保存redis中的数据不同,AOF通过保存redis执行的写命令来记录数据库的状态。如果AOF文件记录了有史以来的所有命令,则将这些命令在一个空的redis重播
一遍,就可以恢复数据
同步策略
一条写命令从产生到写入AOF文件需经过以下三步:
命令追加
,文件写入
,文件同步
一条写命令在被执行完毕后,会被追加到内存缓冲区
接下来就到了事件循环的末尾,一次事件循环的伪代码如下所示:
func eventLoop() {
for {
// 处理文件事件,即客户的请求读写,也就是在这里面完成命令追加
processFileEvents()
// 处理时间事件,例如定期删除过期键
processTimeEvents()
// 根据配置,执行文件写入或文件同步
flushAppendOnlyFile()
}
}
在事件循环的开头,会执行文件事件,即客户的请求读写,也就是在这里面完成命令追加
在一次事件循环结束前,redis会根据配置appendfsync
的值来来决定怎么处理之前加入到内存缓冲区的aof命令:
-
always
:将aof_buf缓冲区中的内容全部写入并同步aof文件- 由于每次事件循环都会同步数据到磁盘,总所周知,磁盘的速度比内存慢很多,因此该配置
效率最低
,但安全性也最高
,因为若宕机最多只会丢失一个事件循环的数据
- 由于每次事件循环都会同步数据到磁盘,总所周知,磁盘的速度比内存慢很多,因此该配置
-
everysec
:将aof_buf缓冲区中的内容全部写入到aof文件,若当前距离上次同步超过1秒,则执行同步- 同步的频率从每次时间循环变为每隔一秒,
效率提升不少
,同时若宕机最多丢失1秒的数据
- 同步的频率从每次时间循环变为每隔一秒,
-
no
:将aof_buf缓冲区中的内容全部写入到aof文件,但不执行同步,何时同步由操作系统决定- 从效率上来说是
最快
的,因此每次都不用等待数据同步到磁盘,但是何时同步数据不可控,有丢失较长时间范围的数据的风险
- 从效率上来说是
文件写入和同步:
现代操作系统为了提升效率,用户将一些数据写入磁盘时(文件写入),操作系统通常会将数据暂存于内存缓冲区,等数据填满或超过一定时限后再真正刷入磁盘。同时操作系统也提供了文件同步的函数
值得一提的是,当配置为always时,并不是每写一条命令就同步一次磁盘,而是一次事件循环后同步一次。因为一次事件循环中可能会执行多条命令
生产环境中通常将appendfsync配置为everysec,在保存高性能的同时尽量减少数据丢失
AOF重写
随着程序的运行,aof文件会越来越大,若不加以处理,数据库重启或宕机时使用aof文件来还原的耗时就会越来越长,甚至超出磁盘容量限制。因此redis会定时为aof文件瘦身
,使其只保存必要的数据
那什么是不必要的数据
呢?
举个例子,假设历史上对list执行了以下6条命令
rpush list "A" // ["A"]
rpush list "B" // ["A","B"]
rpush list "C" // ["A","B","C"]
lpop list // ["B","C"]
lpop list // ["C"]
rpush list "D" "E" // ["C","D","E"]
但这6条命令其实可以用1条命令来替代:
rpush list "C" "D" "E"
这样一来,占用空间和恢复时间都答复减少
进行重写有以下两种方式:
-
分析现有aof文件中有关于list的内容,进行重写
-
读取内存中list的值,用rpush XXX 这一条命令替换掉aof文件中和list有关的命令
- 就像上面的
rpush list "C" "D" "E"
- 就像上面的
很明显,第二种方式实现简单,效率也高,不像第一种方式需要设计复杂的算法来比对,处理aof文件
redis作为单线程应用,如果将aof重写放到主线程中执行,会导致重写期间redis无法处理客户响应
为了避免这种情况,redis将aof文件重写的工作放到子进程中。这么做有以下优点:
- 主线程能继续对外提供服务,不受aof文件重写的影响
- 子进程基于fork那一时刻的数据,不受主线程后续操作的影响,这也是fork子进程方式的通用优点
子进程根据内存快照,生成一份新的aof文件
主线程在子进程重写期间,还在源源不断地接收并执行新的写命令,可能在子进程完成重写后,数据库实际的状态,和重写后的aof文件不一致
。因此需要将这期间新的写命令追加到重写后的aof文件中,再将重写后的aof文件替换到原来的aof文件,这样aof重写才算完成
为了解决数据不一致问题,redis设置了aof重写缓冲区
,在子进程重写期间,新的写命令除了会被写入原aof文件中,还会被写入aof重写缓冲区,这样在子进程重写完毕后,能知道哪些是新命令,将这些新命令追加到重写好的aof文件即可大功告成
为啥新命令还要写到原aof文件中?保证原来的aof持久化逻辑正常运行,当重写失败时不会对原来产生影响
将新命令追加到重写好的aof文件中不需要在子进程中执行,在主线程中执行即可,原因为
- 这些新的写命令不会很多,不会对主线程造成太大影响
- 若再用子进程,还需要考虑怎么合并追加期间的新命令,最终还是需要一个同步操作去合并
混合持久化
单独使用RDB,可能会丢失很多数据,但若单独使用AOF,在恢复数据时相比RDB会慢很多。于是redis 4.0推出了混合持久化,将RDB文件和增量的AOF日志文件放在一起,这里的AOF日志是RDB持久化结束到当前时刻的增量更新日志,通常比较小
这样的混合持久化方式既有了RDB恢复快的优点
,也有AOF不会丢失大量数据的优点
总结
- RDB通过写时复制技术抓取快照进行持久化,配置保存实际时需考虑到各种情况
- 根据业务需要配置AOF同步策略,为了避免文件过大,需要进行AOF文件重写
- 混合持久化方式集成了两者的优点