redis的持久化机制
Redis之所以快,一个最重要的原因在于它是直接将数据存储在内存,并直接从内存中读取数据的,因此一个绝对不容忽视的问题便是,一旦Redis服务器宕机,内存中的数据将会完全丢失。
RDB
- 默认开启的持久化机制,RDB 持久化是指可以将某个时间点上的数据库状态保存到一个 RDB 文件中。RDB 文件是经过压缩的二进制文件,默认的文件名为dump.rdb。因为 RDB 文件是保存在磁盘,所以不会丢失。
手动执行备份
- RDB 持久化可以手动执行(SAVE、BGSAVE),也可以根据服务器配置选项定期执行(save n m)。
- Redis为所有客户端处理数据时使用的是单线程,这个模型就决定了使用者需要尽量避免进行会阻塞主线程的操作。那么Redis在生成RDB文件的时候,会不会阻塞主线程呢?
SAVE 命令时,会阻塞当前 Redis 服务器;
BGSAVE是相当于后台运行,基本上 Redis 内部所有的 RDB 操作都是采用 BGSAVE 命令。 - BGSAVE和SAVE不能同时进行;不然会拒绝请求
BGREWRITEAOF(AOF的持久化方法) 和 BGSAVE 两个命令不能同时执行:①如果 BGSAVE 命令正在执行,那么客户端发送的 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令之执行完毕之后执行;②如果 BGREWRITEAOF 命令正在执行,那么客户端发送的 BGSAVE 命令会被服务器拒绝。
void SAVE(){
# 创建RDB文件
rdbSave();
}
void BGSAVE(){
# 创建子进程
pid = fork();
if (pid==0){
# 子进程创建RDB
rdbSave();
# 创建完成之后向父进程发送信息
signal_parent();
}else if (pid>0){
# 父进程(主线程)继续处理客户端请求,并通过轮询等待子进程的返回信号
handle_request_and_wait_signal();
}else{
# 处理异常
...
}
}
间隔自动备份
# 持久化操作的时机
save 900 1
save 300 10
save 60 10000
# 以上表示“900秒内一个key发送变化”或“300秒了内10个key发生变化”或“60秒内10000个key发生变化”就触发持久化
struct redisServer{
...
// 修改次数的计数器
long dirty;
// 上一次成功执行RDB快照的时间
time_t lastsave;
// 保存条件配置的数组
struct saveparam *saveparams;
...
}
struct saveparam{
// 秒数
time_t seconds;
// 修改次数
int changes;
}
serverCron函数默认每隔100毫秒就会执行一次,该函数的其中一个作用就是检查save命令设置的保存条件是否被满足,是则执行BGSAVE命令;判断当前修改次数和距离上一次执行时间!
问题及思考
- 没隔一段时间进行一次全量数据备份,那么是不是又很多rbd文件?
其实并不是,他就只有一个rbd文件,在备份后覆盖写入同一个rbd文件中;一下两个图片是备份前后的dump.rdb文件对比;
2.BGSAVE的时候是不阻塞主线程的,在备份的同时,也能进行写操作,那他是这么做到备份某一个时间节点的数据的?
这其中用到了一个概念,写时复制(COW);
如果主进程要修改Redis内存中某个数据,那么操作系统内核会将被修改的内存数据复制一份(复制的是修改之前的数据),未被修改的内存数据依然被父子两个进程共享,被主进程修改的内存空间归属于主进程,被复制出来的原始数据归属于子进程。如此一来,主进程就可以在快照发生的过程中肆无忌惮地接受数据写入的请求,子进程也仍然能够对某一时刻的内容做快照。
- 写时复制不是Redis自身的特性,而是操作系统提供的技术手段。 操作系统是一切技术的基础,所有技术的革新都必须建立在操作系统支持的基础上;
- 写时复制是建立在短时间内写请求不多的假设之下,如果写请求的量非常巨大,那么内存复制的压力自然也不会小。
- 写时复制,复制的粒度为一个内存页。如果只是修改一个256B的数据,父进程需要读原来的整个内存页,然后再映射到新的物理地址写入。一读一写会造成读写放大。如果内存页越大(例如2MB的大页),那么读写放大也就越严重,对Redis性能造成影响。因此使用Redis的AOF功能时,需要注意页表的大小不要设置的太大。
优缺点
- 优点
(1)适合大规模的数据恢复
(2)对数据完整性和一致性要求不高更适合使用
(3)节省磁盘空间
(4)恢复速度快 - 缺点
(1)Fork会克隆一份内存中的数据,体积会膨胀2倍
(2)虽然Redis在fork时使用了写时复制技术,但是如果数据庞大会十分消耗性能
(3)如果redis意外down掉,会丢失最后一次快照后的所有修改:因为save操作是在一定时间间隔内做多少次操作才会触发,如果在时间段内的操作还没有达到设定的次数就崩溃了,会导致本次的所有操作没有进行持久化操作,修改就会丢失
AOF
通过将所有对数据库的写入命令记录到AOF文件中,达到记录数据库状态的目的
设置参数描述:
# 开启AOF持久化机制,默认是关闭的
appendonly yes
appendfilename "redis.aof"
# 每个操作执行一次
# appendfsync always
# 每秒执行一次
appendfsync everysec
# 根据操作系统和环境,不一定的时间执行一次持久化
#appendfsync no
# 当前AOF文件大小和最后一次重写后的大小之间的比率>=指定的增长百分比则进行重写
# 如100代表当前AOF文件大小是上次重写的两倍时候才重写
auto-aof-rewrite-percentage 100
# AOF文件最小重写大小,只有当AOF文件大小大于该值时候才可能重写,4.0默认配置64mb。
auto-aof-rewrite-min-size 64mb
AOP文件的内容格式
AOF文件不是直接以指令的格式进行存储的,我们以第一条指令RPUSH list 1 2 3 4 5为例,看一下AOF文件实际保存该条指令的格式。
*2
$6
SELECT
$1
3
*7
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
$1
5
select是自动加上去的,*7表示有7的命令,$5表示该命令有几个字符
- 他存储的时所有的操作数据库的命令(查询等不改变数据的命令不会记录)
- 由此可见他的文件会比rbd文件大
AOF文件的生成过程
命令追加
在AOF开启的情况下,Redis会将成功执行的写指令以上文我们讲过的协议格式追加到Redis的aof_buf缓冲区。
struct redisServer {
// ...
// AOF缓冲区
sds aof_buf;
// ...
};
aof_buf 缓冲区保存着所有等待写入到AOF 文件的协议文本。
文件写入
在AOF功能开启的情况下,文件事件会将成功执行之后的写命令追加到aof_buf缓冲区,在主服务进程死循环的最后,会调用flushAppendOnlyFile函数,该函数会将aof_buf中的数据写入到内核缓冲区,然后判断是否应该进行同步。
void eventLoop {
while(true){
// ...
// 文件事件,接受命令请求,返回客户端回执
// 根据aof功能是否开启,决定是否将写命令追加到aof_buf缓冲区
handleFileEvents();
// 将aof_buf数据写入内核缓冲区
// 判断是否需要将数据同步到磁盘
flushAppendOnlyFile();
// ...
}
}
文件同步(同步策略appendfsync)
redis.conf配置文件中appendOnlyFile的选项有三个值可选,对应三种AOF同步策略,分别是
no :同步时机由内核决定。
everysec :每一秒钟同步一次。
always :每执行一个命令同步一次。
no
由操作系统内核决定同步时机,每个写命令执行完,只是先把日志写入AOF文件的内核缓冲区,不马上进行同步,同步只会在以下任意一种情况下被执行:
- Redis 被关闭
- AOF功能被关闭
- 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)
这三种情况下的同步操作都会引起 Redis 主进程阻塞。
everysec
每秒进行一次同步,同样,写命令执行完,只是先把日志写入AOF文件的内核缓冲区,不马上进行同步;理论上每隔1秒把缓冲区中的内容同步到磁盘,且同步操作有单独的子线程进行,因此不会阻塞主进程。
需要注意的是,我们用的是「理论上」这样的措辞,实际运行中该模式对fsync或fdatasync的调用并不是每秒一次,而是和调用flushAppendOnlyFile函数时Redis所处的状态有关。
每当 flushAppendOnlyFile 函数被调用时, 可能会出现以下四种情况:
- 子线程正在执行同步,并且
- 这个同步的执行时间未超过 2 秒,那么程序直接返回 ;
- 这个同步已经执行超过 2 秒,那么程序执行写入操作 ,但不执行新的同步操作 。但是,这时的写入操作必须等待子线程先完成原本的同步操作 ,因此这里的写入操作会比平时阻塞更长时间。
- 子线程没有在执行同步 ,并且
- 上次成功执行同步距今不超过1秒,那么程序执行写入,但不执行同步 ;
- 上次成功执行同步距今已经超过1秒,那么程序执行写入和同步 。
有上面可以看出,在Everysec模式下
- 如果在情况1下宕机,那么我们最多损失小于2秒内的所有数据。
- 如果在情况2下宕机,那么我们损失的数据可能会超过2秒。
因此AOF在Everysec模式下只会丢失 1 秒钟数据的说法实际上并不准确。
always
每个写命令执行完,立刻同步地将日志写回磁盘。此模式下同步操作是由 Redis 主进程执行的,所以在同步执行期间,主进程会被阻塞,不能接受命令请求。
同步策略appendfsync总结
- No的同步操作只会在AOF关闭或Redis关闭时执行, 或由操作系统内核触发。在一般情况下, 这种模式只需要为写入阻塞,因此它的写入性能要比后面两种模式要高, 但是这种性能的提高是以降低安全性为代价的:在这种模式下,如果发生宕机,那么丢失的数据量由操作系统内核的缓存冲洗策略决定。
- Everysec在性能方面要优于Always , 并且在通常情况下,这种模式最多丢失不多于2秒的数据, 所以它的安全性要高于No ,这是一种兼顾性能和安全性的保存方案。
- Always的安全性是最高的,但性能也是最差的,因为Redis必须阻塞直到命令信息被写入并同步到磁盘之后才能继续处理请求。
三种 AOF模式的特性可以总结为如下表格
aof重写
由上文我们知道aof存储的是命令,那么如果我们大量的对同一个key进行修改,就会产生大量的命令,如果用于备份的话我们只需要关注最后的一个状态就好,因此aof就有了重写机制;
aof重写的实现
aof的重写实际上并不是在原本的aof文件上进行修改的,而是重新生成一个文件;为了避免阻塞主线程,导致数据库性能下降,和 AOF 日志由主进程写回不同,重写过程是由子进程执行bgrewriteaof 来完成的。这样处理的最大好处是:
- 子进程进行 AOF重写期间,主进程可以继续处理命令请求;
- 子进程带有主进程的数据副本,操作效率更高。
这里我们又看到了子线程数据副本的概念,其实这和rdb中的写时复制是一样的;
由此可见,这也是相当于将整个库中的数据重新以特定的规则重新生成一个份新的aof文件
aof重写缓冲区
-
这时我们就会想,在aof重写的时候,主线程还能处理请求吗?
答案是能的,主线程还是正常处理请求,还是一样的将命令aof_buf缓冲区->内核缓冲区–>原有的aof文件中写; -
这时候由于aof写的是例外的一个aof文件,他如何写这些重写中处理的命令呢?
这时候就有了重写缓冲区的概念,在把命令往aof_buf缓冲区写的时候,也会往aof重写缓冲区写一份,等子线程将所有的数据都通过命令写入新的aof文件中后,再把aof重写缓冲区中的命令追加到后面;
等所有的步骤完成后,想老的aof文件替换成新的aof文件,就完成了整个aof重写操作