《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读
Posted zhongrui_fzr
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读相关的知识,希望对你有一定的参考价值。
2、RDB持久化
关键字:RDB文件解析,自动间隔性保存
Redis提供RDB持久化功能,可以将Redis在内存中的数据库状态保存到磁盘里,避免数据意外丢失。
RDB持久化可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。
RDB文件生成
RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。
save命令生成RDB文件的方式是阻塞Redis服务器进程,直到RDB文件创建完毕,在服务器阻塞期间,服务器不能处理任何命令请求
BGSAVE命令的方式是派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求
创建RDB文件的实际工作都是由rdb.c/rdbSave函数完成,save命令和bgsave命令会以不同的方式调用这个函数。
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success
*
* 将数据库保存到磁盘上。
*
* 保存成功返回 REDIS_OK ,出错/失败返回 REDIS_ERR 。
*/
int rdbSave(char *filename)
dictIterator *di = NULL;
dictEntry *de;
char tmpfile[256];
char magic[10];
int j;
long long now = mstime();
FILE *fp;
rio rdb;
uint64_t cksum;
// 创建临时文件
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp)
redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
strerror(errno));
return REDIS_ERR;
// 初始化 I/O
rioInitWithFile(&rdb,fp);
// 设置校验和函数
if (server.rdb_checksum)
rdb.update_cksum = rioGenericUpdateChecksum;
// 写入 RDB 版本号
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
// 遍历所有数据库
for (j = 0; j < server.dbnum; j++)
// 指向数据库
redisDb *db = server.db+j;
// 指向数据库键空间
dict *d = db->dict;
// 跳过空数据库
if (dictSize(d) == 0) continue;
// 创建键空间迭代器
di = dictGetSafeIterator(d);
if (!di)
fclose(fp);
return REDIS_ERR;
/* Write the SELECT DB opcode
*
* 写入 DB 选择器
*/
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(&rdb,j) == -1) goto werr;
/* Iterate this DB writing every entry
*
* 遍历数据库,并写入每个键值对的数据
*/
while((de = dictNext(di)) != NULL)
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
// 根据 keystr ,在栈中创建一个 key 对象
initStaticStringObject(key,keystr);
// 获取键的过期时间
expire = getExpire(db,&key);
// 保存键值对数据
if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
dictReleaseIterator(di);
di = NULL; /* So that we don't release it again on error. */
/* EOF opcode
*
* 写入 EOF 代码
*/
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
/* CRC64 checksum. It will be zero if checksum computation is disabled, the
* loading code skips the check in this case.
*
* CRC64 校验和。
*
* 如果校验和功能已关闭,那么 rdb.cksum 将为 0 ,
* 在这种情况下, RDB 载入时会跳过校验和检查。
*/
cksum = rdb.cksum;
memrev64ifbe(&cksum);
rioWrite(&rdb,&cksum,8);
/* Make sure data will not remain on the OS's output buffers */
// 冲洗缓存,确保数据已写入磁盘
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok.
*
* 使用 RENAME ,原子性地对临时文件进行改名,覆盖原来的 RDB 文件。
*/
if (rename(tmpfile,filename) == -1)
redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return REDIS_ERR;
// 写入完成,打印日志
redisLog(REDIS_NOTICE,"DB saved on disk");
// 清零数据库脏状态
server.dirty = 0;
// 记录最后一次完成 SAVE 的时间
server.lastsave = time(NULL);
// 记录最后一次执行 SAVE 的状态
server.lastbgsave_status = REDIS_OK;
return REDIS_OK;
werr:
// 关闭文件
fclose(fp);
// 删除文件
unlink(tmpfile);
redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
if (di) dictReleaseIterator(di);
return REDIS_ERR;
RDB文件生成条件
RDB文件的载入工作是在服务器启动时自动执行的,所以redis没有专门用于载入RDB文件的命令,只要redis服务器在启动时检测到RDB文件存在,就会自动载入RDB文件
因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
- 若服务器开启了AOF持久化功能,那么服务器就优先使用AOF文件还原数据库状态
- 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态
BGSAVE命令执行期间,客户端发送的save和BGSAVE命令都会被服务器拒绝,防止产生竞态条件
从性能考虑,BGREWRITEAOF和BGSAVE命令不能同时执行:
- 若BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行
- BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
当Redis服务器启动时,用户可以通过指定配置文件或传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置默认条件:
- save 900 1
- save 300 10
- save 60 10000
服务器程序会根据save选项设置服务器状态redisSever结构的saveparams属性:
struct redisServer
// 自从上次 SAVE 执行以来,数据库被修改的次数
long long dirty; /* Changes to DB from the last save */
// BGSAVE 执行前的数据库被修改次数
long long dirty_before_bgsave; /* Used to restore dirty on failed BGSAVE */
// 负责执行 BGSAVE 的子进程的 ID
// 没在执行 BGSAVE 时,设为 -1
pid_t rdb_child_pid; /* PID of RDB saving child */
// 记录了保存条件的数组
struct saveparam *saveparams; /* Save points array for RDB */
int saveparamslen; /* Number of saving points */
char *rdb_filename; /* Name of RDB file */
int rdb_compression; /* Use compression in RDB? */
int rdb_checksum; /* Use RDB checksum? */
// 最后一次完成 SAVE 的时间
time_t lastsave; /* Unix time of last successful save */
// 最后一次尝试执行 BGSAVE 的时间
time_t lastbgsave_try; /* Unix time of last attempted bgsave */
// 最近一次 BGSAVE 执行耗费的时间
time_t rdb_save_time_last; /* Time used by last RDB save run. */
// 数据库最近一次开始执行 BGSAVE 的时间
time_t rdb_save_time_start; /* Current RDB save start time. */
// 最后一次执行 SAVE 的状态
int lastbgsave_status; /* REDIS_OK or REDIS_ERR */
int stop_writes_on_bgsave_err; /* Don't allow writes if can't BGSAVE */
// 服务器的保存条件(BGSAVE 自动执行的条件)
struct saveparam
// 多少秒之内
time_t seconds;
// 发生多少次修改,修改数
int changes;
;
- saveparams属性是一个数组,数组中的每个元素都是一个saveparam结构,每个saveparam结构都保存了一个save选项设置的保存条件
- dirty计数器:记录距离上一次成功执行save命令或BGSAVE命令之后,服务器对数据库状态(服务器中所有数据库)进行了多少次修改(包括写入、删除、更新等操作)
- lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行save命令BGSAVE命令的时间
Redis服务器周期性操作函数serverCron默认每隔100毫秒执行一次,该函数用于对正在运行的服务器进行维护,它其中一项工作就是检查save选项所设置的保存条件是否已经满足,若满足,就执行BGSAVE命令
/* This is our timer interrupt, called server.hz times per second.
*
* 这是 Redis 的时间中断器,每秒调用 server.hz 次。
*
* Here is where we do a number of things that need to be done asynchronously.
* For instance:
*
* 以下是需要异步执行的操作:
*
* - Active expired keys collection (it is also performed in a lazy way on
* lookup).
* 主动清除过期键。
*
* - Software watchdog.
* 更新软件 watchdog 的信息。
*
* - Update some statistic.
* 更新统计信息。
*
* - Incremental rehashing of the DBs hash tables.
* 对数据库进行渐增式 Rehash
*
* - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
* 触发 BGSAVE 或者 AOF 重写,并处理之后由 BGSAVE 和 AOF 重写引发的子进程停止。
*
* - Clients timeout of different kinds.
* 处理客户端超时。
*
* - Replication reconnection.
* 复制重连
*
* - Many more...
* 等等。。。
*
* Everything directly called here will be called server.hz times per second,
* so in order to throttle execution of things we want to do less frequently
* a macro is used: run_with_period(milliseconds) ....
*
* 因为 serverCron 函数中的所有代码都会每秒调用 server.hz 次,
* 为了对部分代码的调用次数进行限制,
* 使用了一个宏 run_with_period(milliseconds) ... ,
* 这个宏可以将被包含代码的执行次数降低为每 milliseconds 执行一次。
*/
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData)
//....
/* If there is not a background saving/rewrite in progress check if
* we have to save/rewrite now */
// 既然没有 BGSAVE 或者 BGREWRITEAOF 在执行,那么检查是否需要执行它们
// 遍历所有保存条件,看是否需要执行 BGSAVE 命令
for (j = 0; j < server.saveparamslen; j++)
struct saveparam *sp = server.saveparams+j;
/* Save if we reached the given amount of changes,
* the given amount of seconds, and if the latest bgsave was
* successful or if, in case of an error, at least
* REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */
// 检查是否有某个保存条件已经满足了
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
REDIS_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == REDIS_OK))
redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
// 执行 BGSAVE
rdbSaveBackground(server.rdb_filename);
break;
// ....
RDB文件格式
由上文rdbSave()函数可以看出,一个完整RDB文件所包含的各个部分有
REDIS | db_version | database | EOF | check_sum
- RDB文件的最开头是redis部分,长度为5字节,保存“REDIS”五个字符。通过这五个字符,程序可以在载入文件时,快速检查所载入的文件是否是RDB文件
- db_version长度为4字节,值是一个字符串表示的整数,记录RDB文件的版本号
- database部分包含零个或任意多个数据库,以及各个数据库中的键值对数据,若所有数据库均为空则这个部分也为空,长度为0
- EOF常量,长度1字节,标识RDB文件正文内容的结束。
- check_sum是一个8字节长的无符号整数,保存着一个校验和,是程序对REDIS、db_version、database、EOF四个部分的内容进行计算得出的。
服务器载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或损坏的情况
一个RDB文件的database部分可以保存任意多个非空数据库。每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分:
SELECTDB | db_number | key_value_pairs
- SELECTDB常量的长度为1字节,当读入程序遇到这个值的时候,它知道接下来要读入的将是一个数据库号码
- db_number保存着一个数据库号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或5字节。当程序度日db_number部分之后,服务器会调用select命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。
- key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的不同,key_value_pairs部分的长度也会有所不同。
不带过期时间的键值对在RDB文件中由type、key、value三部分组成。
type记录了value的类型,长度为1字节,值为常量,如REDIS_RDB_TYPE_STRING、REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST等
- type常量代表了一种对象类型或者底层编码,当服务器读入RDB文件中的键值对数据时,程序会根据type的值来决定如何读入和解释value的数据
key总是一个字符串对象
- 根据type类型的不同,以及保存内容长度的不同,保存value的结构和长度也会有所不同
带有过期时间的键值对结构为:
EXPIRETIME | ms | TYPE | key | value
- EXPIRETIME_MS常量的长度为1字节,表示接下来读入的将是一个以毫秒为单位的过期时间
- ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的UNIX时间戳,即键值对的过期时间
value的编码
RDB文件中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构、长度也会有所不同
- 1、字符串对象,type为:REDIS_RDB_TYPE_STRING
字符串对象的编码若为REDIS_ENCODING_INT:对象中保存的是长度不超过32位的整数
ENCODING | Integer
ENCODING的值可以是REDIS_RDB_ENC_INT8、REDIS_RDB_ENC_INT16或者REDIS_RDB_ENC_INT32三个常量中的一个,分别代表RDB文件使用8位、16位或32位保存整数值Integer字符串对象的编码若为REDIS_ENCODING_RAW:说明对象保存的是一个字符串值
若字符串长度小于等于20字节,那么字符串会被原样保存: len | string
若字符串长度大于20字节,那么这个字符串会被压缩之后再保存:
REDIS_RDB_ENC_LZF | compressed_len | origin_len | compressed_string
REDIS_RDB_ENC_LZF常量表示字符串被LZF算法压缩过
- 2、列表对象,type为:REDIS_RDB_TYPE_LIST
value保存的是一个REDIS_ENCODING_LINKEDLIST编码的列表对象,结构:
list_length | item1 | item2 | 。。。| itemn
- 3、集合对象,type为:REDIS_RDB_TYPE_SET
value保存的是一个REDIS_ENCODING_HT编码的集合对象,结构:
set_size | elem1 | elem2 | … | elemn
- 4、哈希表对象,type为:REDIS_RDB_TYPE_HASH
value保存的是一个REDIS_ENCODING_HT编码的集合对象,结构:
hash_size | key_value_pair 1 | … | key_value_pair N
- 5、有序集合对象,type为:REDIS_RDB_TYPE_ZSET
value保存的是REDIS_ENCODING_SKIPLIST编码的有序集合对象,结构:
sorted_set_size | member1|score1 | member2|score2 | … | memberN|scoreN
- 6、INTSET编码的集合,type为:REDIS_RDB_TYPE_STRING
value保存的是一个整数集合对象,RDB保存这种对象的方法是,先将整数集合对象转换为字符串对象,然后将这个字符串对象保存到RDB文件中
如果程序在读入RDB文件的过程中,遇到由整数集合对象转换成的字符串对象,那么程序会根据TYPE值的指示,先读入字符串对象,再将这个字符串对象转换成原来的整数集合对象
- 7、ZIPLIST编码的列表、哈希表或者有序集合,type为:REDIS_RDB_TYPE_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST、REDIS_RDB_TYPE_ZSET_ZIPLIST
value保存的是一个压缩列表对象,RDB文件保存这种对象的方法是:
将压缩列表转换成一个字符串对象
将字符串对象保存到RDB文件
分析RDB文件
可以使用-od命令分析redis服务器产生的RDB文件,该命令可以用给定的格式转存(dump)并打印输入文件,比如,给定-c参数可以以ASCII编码的方式打印输入文件,给定-x参数可以以十六进制的方式打印输入文件
以上是关于《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读的主要内容,如果未能解决你的问题,请参考以下文章
《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读
《Redis设计与实现》[第二部分]单机数据库的实现-C源码阅读