你真的懂Redis的5种基本数据结构吗?这些知识点或许你还需要看看(图文并茂,浅显易懂,建议收藏)
774
2022-05-28
我们小学三年级的时候就知道,redis是一个纯内存存储的中间件,那它宕机会怎么样?数据会丢失吗?答案是可以不丢。 事实上redis为了保证宕机时数据不丢失,提供了两种数据持久化的机制——rdb和aof。
rdb就定期将内存里的数据全量dump到磁盘里,下次启动时就可以直接加载之前的数据了,rdb的问题是它只能提供某个时刻的数据快照,无法保证建立快照后的数据不丢,所以redis还提供了aof。aof全程是Append Only File,它的原理就是把所有改动一条条写到磁盘上。这篇博客我们来重点介绍下rdb持久性的实现,aof留到下一篇博客。
rdb相关源码
在redis中,触发rdb保存主要有以下几种方式。
save命令
我们在redis-cli下直接调用save命令就会触发rdb文件的生成,如果后台没有在子进程在生成rdb,就会调用rdbSave()生成rdb文件,并将其保存在磁盘中。
void saveCommand(client *c) { // 检查是否后台已经有进程在执行save,如果有就停止执行。 if (server.child_type == CHILD_TYPE_RDB) { addReplyError(c,"Background save already in progress"); return; } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (rdbSave(server.rdb_filename,rsiptr) == C_OK) { addReply(c,shared.ok); } else { addReplyErrorObject(c,shared.err); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Redis 的 rdbSave 函数是真正进行 RDB 持久化的函数,它的大致流程如下:
首先创建一个临时文件。
创建并初始化rio,rio是redis对io的一种抽象,提供了read、write、flush、checksum……等方法。
调用 rdbSaveRio(),将当前 Redis 的内存信息全量写入到临时文件中。
调用 fflush、 fsync 和 fclose 接口将文件写入磁盘中。
使用 rename 将临时文件改名为 正式的 RDB 文件。
将server.dirty清零,server.dirty是用了记录在上次生成rdb后有多少次数据变更,会在serverCron中用到。
具体代码如下:
/* rdb磁盘写入操作 */ int rdbSave(char *filename, rdbSaveInfo *rsi) { char tmpfile[256]; char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */ FILE *fp = NULL; rio rdb; int error = 0; snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Failed opening the RDB file %s (in server root dir %s) " "for saving: %s", filename, cwdp ? cwdp : "unknown", strerror(errno)); return C_ERR; } rioInitWithFile(&rdb,fp); // 初始化rio, startSaving(RDBFLAGS_NONE); if (server.rdb_save_incremental_fsync) rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES); // 内存数据dump到rdb if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) { errno = error; goto werr; } /* 把数据刷到磁盘删,确保操作系统缓冲区没有剩余数据 */ if (fflush(fp)) goto werr; if (fsync(fileno(fp))) goto werr; if (fclose(fp)) { fp = NULL; goto werr; } fp = NULL; /* 把临时文件重命名为正式文件名 */ if (rename(tmpfile,filename) == -1) { char *cwdp = getcwd(cwd,MAXPATHLEN); serverLog(LL_WARNING, "Error moving temp DB file %s on the final " "destination %s (in server root dir %s): %s", tmpfile, filename, cwdp ? cwdp : "unknown", strerror(errno)); unlink(tmpfile); stopSaving(0); return C_ERR; } serverLog(LL_NOTICE,"DB saved on disk"); server.dirty = 0; server.lastsave = time(NULL); server.lastbgsave_status = C_OK; stopSaving(1); return C_OK; werr: serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno)); if (fp) fclose(fp); unlink(tmpfile); stopSaving(0); return C_ERR; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
因为redis是单线程模型,所以在save的过程中处理不了请求,单线程模型可以save的过程中不会有数据变化,但save可能会持续很久,这会导致redis无法正常处理读写请求,对于线上服务来说这是非常致命的,所以redis还提供了bgsave命令,它可以在不影响正常读写的情况下执行save操作,我们来看下具体实现。
bgsave命令
bgsave提供了后台生成rdb文件的功能,bg含义就是background,具体怎么实现的? 其实就是调用fork() 生成了一个子进程,然后在子进程中完成了save的过程。
void bgsaveCommand(client *c) { int schedule = 0; /* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite * is in progress. Instead of returning an error a BGSAVE gets scheduled. */ if (c->argc > 1) { if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) { schedule = 1; } else { addReplyErrorObject(c,shared.syntaxerr); return; } } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (server.child_type == CHILD_TYPE_RDB) { addReplyError(c,"Background save already in progress"); } else if (hasActiveChildProcess()) { if (schedule) { server.rdb_bgsave_scheduled = 1; // 如果bgsave已经在执行中了,这次执行会放到serverCron中执行 addReplyStatus(c,"Background saving scheduled"); } else { addReplyError(c, "Another child process is active (AOF?): can't BGSAVE right now. " "Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever " "possible."); } } else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) { addReplyStatus(c,"Background saving started"); } else { addReplyErrorObject(c,shared.err); } } int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) { pid_t childpid; if (hasActiveChildProcess()) return C_ERR; server.dirty_before_bgsave = server.dirty; server.lastbgsave_try = time(NULL); // 创建子进程,redisFork实际就是对fork的封装 if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) { int retval; /* 子进程 */ redisSetProcTitle("redis-rdb-bgsave"); redisSetCpuAffinity(server.bgsave_cpulist); retval = rdbSave(filename,rsi); if (retval == C_OK) { sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB"); } exitFromChild((retval == C_OK) ? 0 : 1); } else { /* 父进程 */ if (childpid == -1) { server.lastbgsave_status = C_ERR; serverLog(LL_WARNING,"Can't save in background: fork: %s", strerror(errno)); return C_ERR; } serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid); server.rdb_save_time_start = time(NULL); server.rdb_child_type = RDB_CHILD_TYPE_DISK; return C_OK; } return C_OK; /* unreached */ }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
bgsave其实就是把save的流程放到子进程里执行,这样就不会阻塞到父进程了。我最开始看到这里的时候有个问题,父进程在持续读写内存的情况下子进程是如何保存某一时刻快照的? 这个redid中没有特殊处理,还是依赖了操作系统提供的fork()。
当一个进程调用fork()时,操作系统会复制一份当前的进程,包括当前进程中的内存内容。所以可以认为只要fork()成功,当前内存中的数据就被全量复制了一份。当然具体实现上内核为了提升fork()的性能,使用了copy-on-write的技术,只有被复制的数据在被父进程或者子进程改动时才会真正拷贝。
serverCron
上面生成rdb的两种方式都是被动触发的,redis也提供定期生成rdb的机制。redis关于rdb生成的配置如下:
save
1
2
3
4
5
定期生成rdb的实现在server.c 中的serverCron中。serverCron是redis每次执行完一次eventloop执行的定期调度任务,里面就有rdb和aof的执行逻辑,rdb相关具体如下:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* . 略去其他代码 */ /* 检测bgsave和aof重写是否在执行过程中 */ if (hasActiveChildProcess() || ldbPendingChildren()) { run_with_period(1000) receiveChildInfo(); checkChildrenDone(); } else { /* If there is not a background saving/rewrite in progress check if * we have to save/rewrite now. */ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; /* 检查是否达到了执行save的标准 */ if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) { serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...", sp->changes, (int)sp->seconds); rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); rdbSaveBackground(server.rdb_filename,rsiptr); break; } } } /* . 略去其他代码 */ /* 如果上次触发bgsave时已经有进程在执行了,就会标记rdb_bgsave_scheduled=1,然后放到serverCron * 中执行 */ if (!hasActiveChildProcess() && server.rdb_bgsave_scheduled && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) { rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) server.rdb_bgsave_scheduled = 0; } /* . 略去其他代码 */ }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
rdb文件格式
rdb的具体文件格式相对比较简单,具体如下:
----------------------------# 52 45 44 49 53 # 魔术 "REDIS" 30 30 30 33 # ASCII码rdb的版本号 "0003" = 3 ---------------------------- FA # 辅助字段 $string-encoded-key # 可能包含多个元信息 $string-encoded-value # 比如redis版本号,创建时间,内存使用量……... ---------------------------- FE 00 # redis db号. db number = 00 FB # 标识db的大小 $length-encoded-int # hash表的大小(int) $length-encoded-int # expire hash表的大小(int) ----------------------------# 从这里开始就是具体的k-v数据 FD $unsigned-int # 数据还有多少秒过期(4byte unsigned int) $value-type # 标识value数据类型(1 byte) $string-encoded-key # key,redis字符串类型(sds) $encoded-value # value, 类型取决于 $value-type ---------------------------- FC $unsigned long # 数据还有多少毫秒过期(8byte unsigned long) $value-type # 标识value数据类型(1 byte) $string-encoded-key # key,redis字符串类型(sds) $encoded-value # value, 类型取决于 $value-type ---------------------------- $value-type # redis数据key-value,没有过期时间 $string-encoded-key $encoded-value ---------------------------- FE $length-encoding # FE标识前一个db的数据结束,然后再加上数据的长度 ---------------------------- ... # 其他redis db中的k-v数据, ... FF # FF rdb文件的结束标识 8-byte-checksum ## 最后是8byte的CRC64校验和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
总结
rdb在一定程度上保证了redis实例在异常宕机时数据不丢,当因为是定期生成的rdb快照,在生成快照后产生的变动无法追加到rdb文件中,所以rdb无法彻底保证数据不丢,为此redis又提供了另外一种数据持久化机制aof,我们将在下篇文章中看到。另外,在执行bgsave的时候高度依赖于操作系统的fork()机制,这也是会带来很大的性能开销的,详见Linux fork隐藏的开销-过时的fork(正传)
参考资料
Redis RDB 持久化详解
https://rdb.fnordig.de/file_format.html
Redis Persistence
Linux fork隐藏的开销-过时的fork(正传)
本文是Redis源码剖析系列博文,同时也有与之对应的Redis中文注释版,有想深入学习Redis的同学,欢迎star和关注。
Redis中文注解版仓库:https://github.com/xindoo/Redis
Redis源码剖析专栏:https://zxs.io/s/1h
如果觉得本文对你有用,欢迎一键三连。
Redis 任务调度
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。