redis4.0新特性(四)-RDB-AOF混合持久化
简介: redis有两种持久化的方式——RDB和AOF,RDB是一份内存快照,AOF则为可回放的命令日志,他们两个各有特点也相互独立。4.0开始允许使用RDB-AOF混合持久化的方式,结合了两者的优点,通过aof-use-rdb-preamble
配置项可以打开混合开关,yes则表示开启,no表示禁用,默认是禁用的,可通过config set修改。
RDB V.S. AOF
1. RDB
RDB文件本质上是一份内存快照,保存了创建RDB文件那个时间点的redis全量数据。具有数据文件小创建、恢复快的优点,但是由于快照的特性无法保存创建RDB之后的增量数据。
2. AOF
AOF文件本质上是一份执行日志保存所有对redis进行更改的命令,增量数据也就随命令写入AOF文件刷盘的策略,由配置项appendfsync控制可以选择”everysec”或”always”。
AOF文件基本上是human-readable的文本,所以其体积相对较大,在从AOF文件恢复数据时就是做日志回放执行AOF文件中记录的所有命令,所以相对RDB而言恢复耗时较长。
随着redis的运行AOF文件会不断膨胀,由aofrewrite机制来防止磁盘空间被撑满详见上一篇文章《redis4.0之利用管道优化aofrewrite》。
RDB-AOF混合持久化
细细想来,aofrewrite时也是先写一份全量数据到新AOF文件中再追加增量,只不过全量数据是以redis命令的格式写入。那么是否可以先以RDB格式写入全量数据,再追加增量日志呢?这样既可以提高aofrewrite和恢复速度,也可以减少文件大小,还可以保证数据的完毕性,整合RDB和AOF的优点。那么现在4.0实现了这一特性——RDB-AOF混合持久化。
aofrewrite
综上所述RDB-AOF混合持久化体现在aofrewrite时,那么我们就从这里开始来看4.0是如何实现的。
回忆下aofrewrite的过程
无论是serverCron触发或者执行BGREWRITEAOF命令,最终redis都会走到rewriteAppendOnlyFileBackground(),
rewriteAppendOnlyFileBackground函数会fork子进程子进程进入rewriteAppendOnlyFile函数来生成新的AOF文件混合持久化就从这里开始。
int rewriteAppendOnlyFile(char *filename) { ... if (server.aof_use_rdb_preamble) { int error; if (rdbSaveRio(&aof,&error,RDB_SAVE_AOF_PREAMBLE,NULL) == C_ERR) { errno = error; goto werr; } } else { if (rewriteAppendOnlyFileRio(&aof) == C_ERR) goto werr; } ... }
可以看到当混合持久化开关打开时,就会进入rdbSaveRio函数先以RDB格式来保存全量数据。 前文说道子进程在做aofrewrite时,会通过管道从父进程读取增量数据并缓存下来,那么在以RDB格式保存全量数据时也会从管道读取数据,并不会造成管道阻塞。
int rdbSaveRio(rio *rdb, int *error, int flags, rdbSaveInfo *rsi) { ... snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION); if (rdbWriteRaw(rdb,magic,9) == -1) goto werr; if (rdbSaveInfoAuxFields(rdb,flags,rsi) == -1) goto werr;
首先把RDB的版本(注意不是redis的版本)和辅助域写入文件
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) return C_ERR; /* Write the SELECT DB opcode */ if (rdbSaveType(rdb,RDB_OPCODE_SELECTDB) == -1) goto werr; if (rdbSaveLen(rdb,j) == -1) goto werr; /* Write the RESIZE DB opcode. We trim the size to UINT32_MAX, which * is currently the largest type we are able to represent in RDB sizes. * However this does not limit the actual size of the DB to load since * these sizes are just hints to resize the hash tables. */ uint32_t db_size, expires_size; db_size = (dictSize(db->dict) <= UINT32_MAX) ? dictSize(db->dict) : UINT32_MAX; expires_size = (dictSize(db->expires) <= UINT32_MAX) ? dictSize(db->expires) : UINT32_MAX; if (rdbSaveType(rdb,RDB_OPCODE_RESIZEDB) == -1) goto werr; if (rdbSaveLen(rdb,db_size) == -1) goto werr; if (rdbSaveLen(rdb,expires_size) == -1) goto werr;
然后遍历DB先把dbnum和db_size、expires_size写入文件
/* Iterate this DB writing every entry */ while((de = dictNext(di)) != NULL) { sds keystr = dictGetKey(de); robj key, *o = dictGetVal(de); long long expire; initStaticStringObject(key,keystr); expire = getExpire(db,&key); if (rdbSaveKeyValuePair(rdb,&key,o,expire,now) == -1) goto werr; /* When this RDB is produced as part of an AOF rewrite, move * accumulated diff from parent to child while rewriting in * order to have a smaller final write. */ if (flags & RDB_SAVE_AOF_PREAMBLE && rdb->processed_bytes > processed+AOF_READ_DIFF_INTERVAL_BYTES) { processed = rdb->processed_bytes; aofReadDiffFromParent(); } } dictReleaseIterator(di); } di = NULL; /* So that we don't release it again on error. */
在当前DB中遍历所有的key把key-value对及过期时间(如果有设置的话写入文件)。这里小插曲一下在rdbSaveKeyValuePair函数中会判断expire是否已经到了过期时间如果已经过期就不会写入文件,同时如果flags标记了RDB_SAVE_AOF_PREAMBLE的话,说明是在aofrewrite且开启了RDB-AOF混合开关,此时就会从父进程去读取增量数据了。
/* EOF opcode */ if (rdbSaveType(rdb,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. */ cksum = rdb->cksum; memrev64ifbe(&cksum); if (rioWrite(rdb,&cksum,8) == 0) goto werr; return C_OK; }
最后把代表RDB格式结束的RDB_OPCODE_EOF标记和校验和写入文件。
RDB-AOF混合持久化的RDB部分到此结束rdbSaveRio函数运行完后返回rewriteAppendOnlyFile继续把增量数据写入AOF文件。
也就是说AOF文件的前半段是RDB格式的全量数据后半段是redis命令格式的增量数据。
数据恢复
当appendonly配置项为no时,redis启动后会去加载RDB文件,以RDB格式来解析RDB文件自然没有问题。
而appendonly配置项为yes时,redis启动后会加载AOF文件来恢复数据,如果持久化时开启了RDB-AOF混合开关,那么AOF文件的前半段就是RDB格式,此时要如何正确加载数据呢?
一切数据都逃不过协议二字,不以正确的协议存储和解析那就是乱码,既然允许RDB-AOF混合持久化就要能够识别并恢复数据。这一节我们来介绍如何以正确的姿势来恢复数据。
加载AOF文件的入口为loadAppendOnlyFile。
int loadAppendOnlyFile(char *filename) {
...
/* Check if this AOF file has an RDB preamble. In that case we need to
* load the RDB file and later continue loading the AOF tail. */
char sig[5]; /* "REDIS" */
if (fread(sig,1,5,fp) != 5 || memcmp(sig,"REDIS",5) != 0) {
/* No RDB preamble, seek back at 0 offset. */
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
} else {
/* RDB preamble. Pass loading the RDB functions. */
rio rdb;
serverLog(LL_NOTICE,"Reading RDB preamble from AOF file...");
if (fseek(fp,0,SEEK_SET) == -1) goto readerr;
rioInitWithFile(&rdb,fp);
if (rdbLoadRio(&rdb,NULL) != C_OK) {
serverLog(LL_WARNING,"Error reading the RDB preamble of the AOF file, AOF loading aborted");
goto readerr;
} else {
serverLog(LL_NOTICE,"Reading the remaining AOF tail...");
}
}
...
}
打开AOF文件之后首先读取5个字符,如果是”REDIS”,那么就说明这是一个混合持久化的AOF文件,正确的RDB格式一定是以”REDIS”开头,而纯AOF格式则一定以”*“开头,此时就会进入rdbLoadRio函数来加载数据。
rdbLoadRio函数此处就不详细展开了,就是以约定好的协议解析文件内容,直至遇到RDB_OPCODE_EOF结束标记,返回loadAppendOnlyFile函数继续以AOF格式解析文件直到结束整个加载过程完成。
附录
1. RDB格式的文件
我们先向redis写入一些数据并生成RDB文件
$redis-cli
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> expire foo 60
(integer) 1
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> set foo bar
OK
127.0.0.1:6379[2]> bgsave
Background saving started
看下RDB文件内容
$cat dump.rdb
REDIS0008 redis-ver4.0.1
redis-bits@ctimeYused-mem
aof-preamblerepl-id(484f9d49a700c4b9b136f0fd40d2d6e5a8460438
repl-offa;^foobarfoobar^KJ_U
OMG…一堆乱码隐约可以看到一些和redis相关的字符串为了更直观的感受下RDB的内容我们用redis自带的工具redis-check-rdb来看下
redis-check-rdb dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '4.0.1'
[offset 40] AUX FIELD redis-bits = '64'
[offset 52] AUX FIELD ctime = '1504234774'
[offset 67] AUX FIELD used-mem = '2139016'
[offset 83] AUX FIELD aof-preamble = '0'
[offset 133] AUX FIELD repl-id = '484f9d49a700c4b9b136f0fd40d2d6e5a8460438'
[offset 148] AUX FIELD repl-offset = '0'
[offset 150] Selecting DB ID 0
[offset 173] Selecting DB ID 2
[offset 194] Checksum OK
[offset 194] \o/ RDB looks OK! \o/
[info] 2 keys read
[info] 1 expires
[info] 0 already expired
这下就好看多了,首先可以看到是一些AUX FIELD辅助域,4.0特有用来配合全新的主从同步方式PSYNC2,后面会专门来介绍PSYNC2,然后可以看到DB0和DB2是有内容的,Checksum也OK。最后是说一共有2个key,其中一个设置了过期时间到目前为止还都没有过期。
2. AOF格式的文件
同样的数据我们来看下在AOF文件中是如何保存的
$cat appendonly.aof
*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$3
bar
*3
$9
PEXPIREAT
$3
foo
$13
1504255377087
*2
$6
SELECT
$1
2
*3
$3
set
$3
foo
$3
bar
很明显就是一条一条redis命令。
3. RDB-AOF混和持久化的文件
最后来看下RDB-AOF混和持久化的文件。
首先打开混合开关执行BGREWRITEAOF生成RDB-AOF混合文件,再追加写入一些数据
$redis-cli
127.0.0.1:6379> config set aof-use-rdb-preamble yes
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> quit
再来看下此时AOF文件内容
$cat appendonly.aof
REDIS0008 redis-ver4.0.1
redis-bits@ctimeYused-memP
aof-preamblerepl-id(484f9d49a700c4b9b136f0fd40d2d6e5a8460438
repl-offsetfoobar?I Y*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$3
bar
显而易见前半段是RDB格式的全量数据后半段是redis命令格式的增量数据。
优缺点
RDB
优点:
- RDB 是一个非常紧凑(compact)的文件,体积小,因此在传输速度上比较快,因此适合灾难恢复。
- RDB 可以最大化Redis 的性能:父进程在保存RDB 文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘I/O 操作。
- RDB 在恢复大数据集时的速度比AOF 的恢复速度要快。
缺点:
RDB是一个快照过程,无法完整的保存所有数据,尤其在数据量比较大时候,一旦出现故障丢失的数据将更多。
当redis中数据集比较大时候,RDB由于RDB方式需要对数据进行完成拷贝并生成快照文件,fork的子进程会耗CPU,并且数据越大,RDB快照生成会越耗时。
RDB文件是特定的格式,阅读性差,由于格式固定,可能存在不兼容情况。
AOF
优点:
- 数据更完整,秒级数据丢失(取决于设置fsync策略)。
- 兼容性较高,由于是基于redis通讯协议而形成的命令追加方式,无论何种版本的redis都兼容,再者aof文件是明文的,可阅读性较好。
缺点:
- 数据文件体积较大,即使有重写机制,但是在相同的数据集情况下,AOF文件通常比RDB文件大。
- 相对RDB方式,AOF速度慢于RDB,并且在数据量大时候,恢复速度AOF速度也是慢于RDB。
- 由于频繁地将命令同步到文件中,AOF持久化对性能的影响相对RDB较大。
混合持久化
优点:
混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,减少数据丢失。
缺点:
兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。