Redis源码剖析之持久化

栏目: 数据库 · 发布时间: 5年前

内容简介:Redis提供了两种持久化方式:RDB和AOF,下面,我们来看看上述两者的底层实现原理。在Redis中,有两种方式可以生成RDB文件,一个是SAVE,另一个是BGSAVE两者的主要区别是:

Redis提供了两种持久化方式:RDB和AOF,下面,我们来看看上述两者的底层实现原理。

一,RDB持久化

1.RDB文件的创建与载入

Redis 中,有两种方式可以生成RDB文件,一个是SAVE,另一个是BGSAVE

两者的主要区别是: SAVE命令在进行持久化操作的过程中,会阻塞Redis服务进行 ,也就是说,在以SAVE方式进行持久化操作的过程中,服务器不能再处理其他的命令请求,这个请求过程必须等到持久化操作结束; BGSAVE命令则是单独开启一个子进程来处理持久化操作

上述过程用伪代码表现形式如下:

def save():

rdbSave() # 将数据写入文件操作

def bgsave():

# 创建子进程    pid = fork()

if pid == 0:

# 子进程负责创建RDB文件        rdbSave()

# 完成之后向父进程发送信号        signal_parent()

elif pid > 0:

# 父进程继续处理命令请求,并通过轮询等待子进程信号        handle_request_and_wait_signal()

else:

# 处理出错情况        handle_fork_error()

RDB文件的载入是在Redis服务器启动时,自动载入的,所以Redis并没有专门用于载入RDB文件的命令。只要服务器检测到有RDB文件的存在,它就会自动进行载入 操作。

关于RDB文件载入过程,值得提一下就是,如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库。

只有在AOF持久化功能处于关闭状态,Redis服务器才会使用RDBRDB文件来还原数据库状态。

2.执行save,bgsave命令时,服务器的状态

在执行save命令时,redis服务器会被阻塞,所以当save命令正在被执行时,客户端发送的所有命令请求都会被拒绝。

在执行bgsave命令时,由于是子进程在处理持久化操作,所以Redis服务器可以继续处理客户的命令请求。但是,在执行bgsave命令期间,如果客户端又发送来了save,bgsave,bgrewrteaof三个命令其中一个,那么服务器的处理方式会有所不同。

首先,bgsave 命令正在被子进程执行,那么客户端发来的save命令会直接被服务器拒绝,这是为了避免父进程与子进程同时执行两个rdbSave()调用,防止产生竞争条件。

其次,bgsave 命令正在被子进程执行,那么客户端发来的bgsave命令也会直接被服务器拒绝,同样也是为了防止产生竞争条件。

最后,bgsave命令和bgrewrteaof命令不能同时进行,如果bgsave命令正在执行,客户端的bgrewrteaof命令会延迟到bgsave命令执行完毕以后才会执行;如果bgrewrteaof命令正在被执行,那么客户端的bgsave命令会直接被服务器拒绝。这是因为,这两个命令都是由子进程来执行的,不能同时执行主要考虑到性能问题,试想两个并发执行的命令,同时进行大量的读写磁盘操作,这会大大降低服务器性能。

3.间隔性保存

上述我们讲到,save命令会阻塞服务器进程,而bgsave命令则会另启一个进程来执行持久化操作。

因为bgsave命令可以在不阻塞服务器进程来进行持久化,所以redis允许用户通过设置服务器配置的save选项,来让redis间接性的自动执行bgsave命令。

用户可以在redis.conf文件配置save保存规则,只要其中一个条件满足,服务器就会自动执行bgsave命令。

save 900 1 # 900秒之内,对数据库进行了一次修改就执行bgsave命令

save 300 10 # 300秒之内,对数据库进行了十次修改就执行bgsave命令

save 60 10000 # 60秒之内,对数据库进行了一万次修改就执行bgsave命令接下来,我们来看看服务器是如何根据上述配置的规则,自动执行bgsave命令。

我们来看看源码redis.h/redisServer,在这个大的结构体中存在如下一个字段:

struct redisServer{

...

struct saveparam *saveparams; //记录了保存条件的数组

...

};

服务器会根据save选项所设置的保存条件,将该值设置到服务器redisServer结构的saveparams属性:

saveparams属性是一个数组,数组每一个元素都是一个saveparam结构,每个结构都保存了一个save选择设置的保存条件:

struct saveparam{

//秒数

time_t seconds;

//修改数

int changes;

};

上述结构体中的两个参数就是我们设置的,如:save 600 1; 那么seconds=600,changes=1。是不是很神奇!!

如果有多个条件同时存在的话,那么它的结构如下:

Redis源码剖析之持久化

除了saveparms数组之外,服务器还维持着两个参数:dirty和lastsave.

其中,dirty记录上一次执行save或者bgsave命令,服务器对数据库状态进行了多少次修改。lastsave则记录上一次执行save或者bgsave命令的时间。

struct redisServer{

// 修改计数器    long long dirty;

// 上一次执行保存的时间    time_t lastsave;

struct saveparam *saveparams; //记录了保存条件的数组};

说完了上述,接下来就来说说,redis服务器是如何发现该执行保存操作呢?

在redis服务器启动之后,内部定期执行执行一个时间事件函数serverCron,这个函数默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是检查save选项设置的保存条件是否满足,如果满足,就执行bgsave命令。

伪代码如下:

def serverCron():

# ...    # 遍历所有保存条件    for saveparam in server.saveparams:

#计算具体上次执行保存操作有多少秒        save_interval = unixtime_now() - server.lastsave

# 如果数据库状态的修改次数超过条件所设置的次数        # 并且距离上次保存的时间超过条件所设置的时间        # 那么执行保存操作

if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:

BGSAVE()

# ...   

以上就是redis服务器根据save选项所设置的保存条件,自动执行bgsave命令,进行间隔性数据保存的实现原理。

二,AOF持久化

RDB持久化是通过保存数据库中的键值对来记录数据库状态,而AOF持久化则是通过保存Redis服务器所执行的写命令来记录数据状态 (如:set key "hello world"  以RDB持久化方式,文件内容为key:hello world,以AOF持久化方式,文件内容为set key "hello world")。

接下来,我们来看看 AOF持久化的实现原理 以及 减小AOF文件体积的AOF文件重写实现原理

1.AOF持久化实现

这里,我们先说说AOF持久化操作,写入文件的操作并不是单单将命令写入,如set key "hello world",而是将命令按照某种格式进行写入,至于为什么要这样做,后面我们再说。写入文件的内容以某个格式,我们称为协议格式。如上面的命令,则写入文件的如下:*2\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nhello\r\n$5\r\nworld

AOF持久化分为三个步骤:命令追加,文件写入,文件同步

命令追加

当AOF持久化功能处于打开状态,服务器在执行完一个写命令之后,会以协议格式的形式将被执行的命令追加到服务器aof_buf缓冲区,至于为什么要写入,后面介绍。

struct redisServer{

sds aof_buf; // 写入缓冲区

};

文件写入与同步

Redis是单线程架构,也就是说redis服务进程处于一个事件循环中,这个事件循环负责接受来自客户端的命令,以及向客户端发送命令,而时间事件则负责想serverCron函数这样需要定时运行的函数。

因为服务器在处理文件事件时,可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_bug缓冲区中的内容写入和保存到AOF文件里面,这个过程可用如下代码描述:

def event_loop():

while True:

# 处理文件事件,接收命令请求以及发送命令回复        # 处理命令请求时可能会有新内容被追加到aof_buf缓冲区中        processFileEvents()

# 处理函时间事件        processTimeEvents()

# 考虑是否将aof_buf中的内容写入和保存到AOF文件里面

flushAppendOnlyFile()

而flushAppendOnlyFile函数行为由服务器配置redis.conf中的appendsync选项的值来决定。

appendsync=always/everysec(默认)/no

2.AOF文件的载入与数据还原

因为AOF文件里面包含了重键数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

具体还原过程:

创建一个不带网络连接的伪客户端,因为redis命令只能在客户端上下文中执行,而载入AOF文件所使用的命令直接来源AOF文件而不是网络连接,所以服务器使用了一个伪客户端来执行AOF文件保存的写命令,效果与客户端执行命令一样。

从AOF文件中分析并读取一条写命令。

使用伪客户端执行被读出的命令。

重复上述步骤。

3.AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容越来越多,文件的体积也会越来越大,如果不加以控制的话,过大的AOF文件可能对Redis服务器,甚至整个宿主计算机造成影响���并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

如:

>rpush list 'a' 'b'

>rpush list 'c'

>rpush list 'd'

>rpush list 'e'

上述光是记录list状态,AOF文件就要保存五条命令。为了解决上述问题,Redis提供了AOF文件重写功能。

AOF文件重写并不需要对现有的AOF文件进行任何读取操作,而是根据现有的数据库状态,将其再次进行持久化操作,然后替换保存之前的文件。

例如上述四条命令是文件记录的,将其还原到redis数据,那么保存在redis数据库中的是如下情景list-->['a','b','c','d','e'],现在我们要进行重写,则根据数据构造出命令:rpush list 'a' 'b' 'c' 'd' 'e'。这样我通过1条命令来代替上面的4条命令,从而大大节约了空间。这就是AOF文件重写功能。

整个重写过程可用如下伪代码表示:

def aof_rewrite(new_aof_file_name):

# 创建新AOF文件

f = create_file(new_aof_file_name)

# 遍历数据库

for db in redisServer.db:

# 忽略空数据库

if db.is_empty():continue

# 写入Select命令,指定数据号码

f.write_command("SELECT" + db.id)

# 遍历数据库中的所有键

for key in db:

# 忽略已过期的键

if key.is_expired():continue

# 根据键的类型对键进行重写

if key.type == String:

rewrite_string(key)

elif key.type == List:

rewrite_list(key)

elif key.type == Hash:

rewrite_hash(key)

elif key.type == Set:

rewrite_set(key)

elif key.type == SortedSet:

rewrite_sorted_set(key)

# 如果键带有过期时间,那么过期时间也要被重写

if key.have_expire_time():

rewrite_expire_time(key)

f.close()

def rewrite_string(key):

# 使用GET命令获取字符串键的值

value = GET(key)

# 使用SET命令重写字符串键

f.write_command(SET,key,value)

def rewrite_list(key):

# 使用LRANGE命令获取列表键包含的所有元素

item1,item2,...,itemN = LRANGE(key,0,-1)

# 使用RPUSH命令重写列表键

f.write_command(RPUSH,key,item1,item2,....,itemN)

def rewrite_hash(key):

field1,value1,field2,value2,...,fieldN,valueN = HGETALL(key)

f.write_command(HSET,key,field1,value1,field2,value2,...,fieldN,valueN)

def rewrite_set(key):

elem1,elem2,...,elemN = SMEMBERS(key)

f.write_command(SADD,key,elem1,elem2,...,elemN)

def rewrite_sorted_set(key):

member1,score1,member2,score2,...,memberN,scoreN = ZRANGE(key,0,-1,"WITHSCORES")

f.write_command(member1,score1,member2,score2,...,memberN,scoreN)

def rewrite_expire_time(key):

timestamp = get_expire_time_in_unixstamp(key)

f.write_command(pexpireat,key,timestamp)

Linux公社的RSS地址https://www.linuxidc.com/rssFeed.aspx

本文永久更新链接地址: https://www.linuxidc.com/Linux/2018-11/155236.htm


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

民事诉讼程序研究

民事诉讼程序研究

乔罗威茨 / 吴泽勇 / 2008-6 / 40.00元

《民事诉讼程序研究》共分为诉讼程式;扩散利益、分散利益和集体利益的保护;程式样式;当事人与法官;对判決的救济;程式改革。主要內容包括:民事诉讼;英美民事诉讼程式在20世纪的若干发展;论民事诉讼法的本质和目的等。一起来看看 《民事诉讼程序研究》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具