Redis源码剖析之RDB

网友投稿 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

Redis源码剖析之RDB

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 ## 例如 save 3600 1 # 3600秒内如果有1条写书就生成rdb save 300 100 # 300秒内如果有100条写书就生成rdb save 60 10000 # 60秒内如果有1000条写书就生成rdb

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小时内删除侵权内容。

上一篇:240_Redis_数据持久化RDB_AOF
下一篇:如何在鲲鹏服务器快速部署docker+docker-compose环境
相关文章