Redis RDB持久化

在运行的情况下,redis将数据以数据结构的形式保存在内存中,为了让数据在redis重启之后仍然可用,Redis提供了RDB和AOF两种持久化方式,本篇讲解RDB持久化的工作方式

在运行时,redis将内存中的数据库以快照的方式存储到磁盘中,在重启后读取磁盘中RDB文件来将redis恢复到原来的状态

RDB的优缺点

优点

·RDB是一个非常紧凑的文件,它保存了redis在某个时间点上的数据集,非常适于备份

·RDB适用于灾难恢复,因为其只有一个文件,而且内容紧凑,可以将其传送到其他的数据中心用于保存

·RDB可以最大化redis的性能,执行RDB持久化时只需要fork一个子进程,并由子进程进行持久化工作,父进程不需要处理任何磁盘I/O操作

·RDB在恢复大数据集时比AOF要快

缺点

·如果你尽量避免服务器故障时丢失数据,那么RDB并不适合。因为RDB持久化要保存整个数据集的数据,这不是一个轻松的操作,所以不适合频繁操作,一旦服务器发生故障有可能会丢失几分钟的数据

·每次保存RDB时,父进程都需要fork一个子进程进行持久化的操作,如果数据集非常的庞大,fork所需的时间可能非常长,这样可能造成暂停处理客户端的请求

如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失

RDB快照

在默认情况下,Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置,让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时,自动保存一次数据集。你也可以通过调用SAVE 或者BGSAVE ,手动让 Redis 进行数据集保存操作
比如说,以下设置会让 Redis 在满足“60 秒内有至少有 1000 个键被改动”这一条件时,自动保存一次数据集:
save 60 1000
这种持久化方式被称为快照(snapshot)

格式

一个RDB文件可以分为下面几个部分:
image

REDIS: 文件开头保存着 REDIS 5个字符,标示这一个RDB文件的开始

RDB-VERSION:一个四字节长的以字符表示的整数,记录了该文件所使用的 RDB 版本号

DB-DATA:这一部分会在一个RDB文件中出现不止一次,这个部分保存了redis中非空数据库的所有数据

SELECT-DB:这个字段保存着后面键值对所属的数据库序号

KEY-VALUE-PAIRS:因为空的数据库不会被保存到 RDB 文件,所以这个部分至少会包含一个键值对的数据。

每个键值对的数据使用以下结构来保存:

image

OPTIONAL-EXPIRE-TIME 域是可选的,如果键没有设置过期时间,那么这个域就不会出现;反之,如果这个域出现的话,那么它记录着键的过期时间,在当前版本的 RDB 中,过期时间是一个以毫秒为单位的 UNIX 时间戳

KEY 域保存着键,格式和 REDIS_ENCODING_RAW 编码的字符串对象一样
TYPE-OF-VALUE 域记录着 VALUE 域的值所使用的编码,根据这个域的指示,程序会使用不同的方式来保存和读取 VALUE 的值

EOF:标示数据库内容的结尾,EDIS_RDB_OPCODE_EOF

CHECK-SUM:RDB 文件所有内容的校验和,一个 uint_64t 类型值
REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾,当读取时,根据它的值对内容进行校验

RDB持久化命令

SAVE

当SAVE命令执行时,Redis服务器是阻塞的,所以当一个SAVE命令执行时,不会处理另外的SAVE、BGSAVE或者BGREWRITEAOF命令

只有在上一个 SAVE 执行完毕、Redis 重新开始接受请求之后,新的 SAVE 、BGSAVE 或BGREWRITEAOF 命令才会被处理
另外,因为 AOF 写入由后台线程完成,而 BGREWRITEAOF 则由子进程完成,所以在 SAVE执行的过程中,AOF 写入和 BGREWRITEAOF 可以同时进行

BGSAVE

在执行 SAVE 命令之前,服务器会检查 BGSAVE 是否正在执行当中,如果是的话,服务器就不调用 rdbSave ,而是向客户端返回一个出错信息,告知在 BGSAVE 执行期间,不能执行SAVE
这样做可以避免 SAVE 和 BGSAVE 调用的两个 rdbSave 交叉执行,造成竞争条件

另一方面,当 BGSAVE 正在执行时,调用新 BGSAVE 命令的客户端会收到一个出错信息,告
知 BGSAVE 已经在执行当中
BGREWRITEAOF 和 BGSAVE 不能同时执行:
• 如果 BGSAVE 正在执行,那么 BGREWRITEAOF 的重写请求会被延迟到 BGSAVE 执行完毕之后进行,执行 BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
• 如果 BGREWRITEAOF 正在执行,那么调用 BGSAVE 的客户端将收到出错信息,表示这两个命令不能同时执行。
BGREWRITEAOF 和 BGSAVE 两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑:并发出两个子进程,并且两个子进程都同时进行大量的磁盘写入操作,这怎么想都不会是一个好主意

RDB的实现

RDB 功能最核心的是 rdbSave 和 rdbLoad 两个函数

image

下面具体分析rdbSave函数的实现

redis 支持两种方式进行 RDB:

当前进程执行和后台执行(BGSAVE)

RDB BGSAVE 策略是 fork 出一个子进程,把内存中的数据集整个 dump 到硬盘上。两个场景举例

  1. redis 服务器初始化过程中,设定了定时事件,每隔一段时间就会触发持久化操作;进入定时事件处理程序中,就会 fork 产生子进程执行持久化操作
  2. redis 服务器预设了 save 指令,客户端可要求服务器进程中断服务,执行持久化操作

rdbSave函数实现如下:

  1: /* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
  2: /*
  3:  * 将数据库保存到磁盘上。成功返回 REDIS_OK ,失败返回 REDIS_ERR 。
  4:  */
  5: int rdbSave(char *filename) {
  6:     dictIterator *di = NULL;
  7:     dictEntry *de;
  8:     char tmpfile[256];
  9:     char magic[10];
 10:     int j;
 11:     long long now = mstime();
 12:     FILE *fp;
 13:     rio rdb;
 14:     uint64_t cksum;
 15: 
 16:     // 以 "temp-<pid>.rdb" 格式创建临时文件名
 17:     snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
 18:     fp = fopen(tmpfile,"w");
 19:     if (!fp) {
 20:         redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
 21:             strerror(errno));
 22:         return REDIS_ERR;
 23:     }
 24: 
 25:     // 初始化 rio 文件
 26:     rioInitWithFile(&rdb,fp);
 27:     // 如果有需要的话,设置校验和计算函数
 28:     if (server.rdb_checksum)
 29:         rdb.update_cksum = rioGenericUpdateChecksum;
 30:     // 以 "REDIS <VERSION>" 格式写入文件头,以及 RDB 的版本
 31:     snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
 32:     if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
 33: 
 34:     // 遍历所有数据库,保存它们的数据
 35:     for (j = 0; j < server.dbnum; j++) {
 36:         // 指向数据库
 37:         redisDb *db = server.db+j;
 38:         // 指向数据库 key space
 39:         dict *d = db->dict;
 40:         // 数据库为空, pass ,处理下个数据库
 41:         if (dictSize(d) == 0) continue;
 42: 
 43:         // 创建迭代器
 44:         di = dictGetSafeIterator(d);
 45:         if (!di) {
 46:             fclose(fp);
 47:             return REDIS_ERR;
 48:         }
 49: 
 50:         /* Write the SELECT DB opcode */
 51:         // 记录正在使用的数据库的号码
 52:         if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
 53:         if (rdbSaveLen(&rdb,j) == -1) goto werr;
 54: 
 55:         /* Iterate this DB writing every entry */
 56:         // 将数据库中的所有节点保存到 RDB 文件
 57:         while((de = dictNext(di)) != NULL) {
 58:             // 取出键
 59:             sds keystr = dictGetKey(de);
 60:             // 取出值
 61:             robj key, 
 62:                  *o = dictGetVal(de);
 63:             long long expire;
 64:             
 65:             initStaticStringObject(key,keystr);
 66:             // 取出过期时间
 67:             expire = getExpire(db,&key);
 68:             if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
 69:         }
 70:         dictReleaseIterator(di);
 71:     }
 72:     di = NULL; /* So that we don't release it again on error. */
 73: 
 74:     /* EOF opcode */
 75:     if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
 76: 
 77:     /* CRC64 checksum. It will be zero if checksum computation is disabled, the
 78:      * loading code skips the check in this case. */
 79:     cksum = rdb.cksum;
 80:     memrev64ifbe(&cksum);
 81:     rioWrite(&rdb,&cksum,8);
 82: 
 83:     /* Make sure data will not remain on the OS's output buffers */
 84:     fflush(fp);
 85:     fsync(fileno(fp));
 86:     fclose(fp);
 87: 
 88:     /* Use RENAME to make sure the DB file is changed atomically only
 89:      * if the generate DB file is ok. */
 90:     // 将临时文件 tmpfile 改名为 filename 
 91:     if (rename(tmpfile,filename) == -1) {
 92:         redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
 93:         unlink(tmpfile);
 94:         return REDIS_ERR;
 95:     }
 96: 
 97:     redisLog(REDIS_NOTICE,"DB saved on disk");
 98: 
 99:     // 初始化数据库数据
100:     server.dirty = 0;
101:     server.lastsave = time(NULL);
102:     server.lastbgsave_status = REDIS_OK;
103: 
104:     return REDIS_OK;
105: 
106: werr:
107:     fclose(fp);
108:     unlink(tmpfile);
109:     redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
110:     if (di) dictReleaseIterator(di);
111:     return REDIS_ERR;
112: }

在rdbSave()中,首先创建一个临时文件,在成功save后会改名,然后初始化rio(robust IO,是对IO操作的包装,在Redis中专门用于ddb save),然后在文件中首先记录REDIS MAGIC NUMBER。这里的rdb_checksum是校验和,每当向硬盘写时,都会更新校验和,用于后面读取rdb文件时校验

dictGetSafeIterator()是得到dict数据结构的一个迭代器,用于遍历Redis Instance中的数据库,首先写入REDIS_RDB_OPCODE_SELECTDB前缀和DB编号。这里的rdbSaveLen()就是实现长度压缩编码的函数,用6bits、14bits或者32bits保存长度。然后遍历DB,获得key和value,还有可能具有的expire time。因为在DB层,key是ads结构的字符串,需要先包装为robj对象,再根据String类型的保存方式保存。调用rdbSaveKeyValuePair()解析value类型,根据类型保存格式

最后写入校验和,同步内容到硬盘,然后重命名文件

以下为bgsave,fork生成一个子进程

  1: 
  2: /*
  3:  * 使用子进程保存数据库数据,不阻塞主进程
  4:  */
  5: int rdbSaveBackground(char *filename) {
  6:     pid_t childpid;
  7:     long long start;
  8: 
  9:     if (server.rdb_child_pid != -1) return REDIS_ERR;
 10:     
 11:     // 修改服务器状态
 12:     server.dirty_before_bgsave = server.dirty;
 13: 
 14:     // 开始时间
 15:     start = ustime();
 16:     // 创建子进程
 17:     if ((childpid = fork()) == 0) {
 18:         int retval;
 19: 
 20:         /* Child */
 21:         // 子进程不接收网络数据
 22:         if (server.ipfd > 0) close(server.ipfd);
 23:         if (server.sofd > 0) close(server.sofd);
 24: 
 25:         // 保存数据
 26:         retval = rdbSave(filename);
 27:         if (retval == REDIS_OK) {
 28:             size_t private_dirty = zmalloc_get_private_dirty();
 29: 
 30:             if (private_dirty) {
 31:                 redisLog(REDIS_NOTICE,
 32:                     "RDB: %lu MB of memory used by copy-on-write",
 33:                     private_dirty/(1024*1024));
 34:             }
 35:         }
 36:         
 37:         // 退出子进程
 38:         exitFromChild((retval == REDIS_OK) ? 0 : 1);
 39:     } else {
 40:         /* Parent */
 41:         // 记录最后一次 fork 的时间
 42:         server.stat_fork_time = ustime()-start;
 43: 
 44:         // 创建子进程失败时进行错误报告
 45:         if (childpid == -1) {
 46:             redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
 47:                 strerror(errno));
 48:             return REDIS_ERR;
 49:         }
 50: 
 51:         redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
 52: 
 53:         // 记录保存开始的时间
 54:         server.rdb_save_time_start = time(NULL);
 55:         // 记录子进程的 id
 56:         server.rdb_child_pid = childpid;
 57:         // 在执行时关闭对数据库的 rehash
 58:         // 避免 copy-on-write
 59:         updateDictResizePolicy();
 60: 
 61:         return REDIS_OK;
 62:     }
 63: 
 64:     return REDIS_OK; /* unreached */
 65: }
 66: 

如果采用 BGSAVE 策略,且内存中的数据集很大,fork() 会因为要为子进程产生一份虚拟空间表而花费较长的时间;如果此时客户端请求数量非常大的话,会导致较多的写时拷贝操作;在 RDB 持久化操作过程中,每一个数据都会导致 write() 系统调用,CPU 资源很紧张。因此,如果在一台物理机上部署多个 redis,应该避免同时持久化操作。

那如何知道 BGSAVE 占用了多少内存?子进程在结束之前,读取了自身私有脏数据 Private_Dirty 的大小,这样做是为了让用户看到 redis 的持久化进程所占用了有多少的空间。在父进程 fork 产生子进程过后,父子进程虽然有不同的虚拟空间,但物理空间上是共存的,直至父进程或者子进程修改内存数据为止,所以脏数据 Private_Dirty 可以近似的认为是子进程,即持久化进程占用的空间

一个函数是zmalloc_get_private_dirty,这是进程fork之后子进程多占用的内存,从/proc/self/smaps中读取Private_Dirty字段的值。这里要说一下Private_Dirty和Private_Clean,进程fork之后,开始内存是共享的,即从父进程那里继承的内存空间都是Private_Clean,运行一段时间之后,子进程对继承的内存空间做了修改,这部分内存就不能与父进程共享了,需要多占用,这部分就是Private_Dirty

RDB写入时,会连内存一起Fork出一个新进程,遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程中又有新的写入请求进来的问题

redis采用了操作系统的Copy-On-Write策略(子进程与父进程共享Page。如果父进程的Page-每页4K有修改,父进程自己创建那个Page的副本,不会影响到子进程,父爱如山)

对于每一个键值对都会调用 rdbSaveKeyValuePair(),如下:

  1: /* Save a key-value pair, with expire time, type, key, value.
  2:  * 保存键值对,值的类型,以及它的过期时间(如果有的话)。
  3:  *
  4:  * On error -1 is returned.
  5:  * 出错返回 -1 。
  6:  *
  7:  * On success if the key was actaully saved 1 is returned, otherwise 0
  8:  * is returned (the key was already expired). 
  9:  *
 10:  * 如果 key 已经过期,放弃保存,返回 0 。
 11:  * 如果 key 保存成功,返回 1 。
 12:  */
 13: int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
 14:                         long long expiretime, long long now)
 15: {
 16:     /* Save the expire time */
 17:     // 保存过期时间
 18:     if (expiretime != -1) {
 19:         /* If this key is already expired skip it */
 20:         // key 已过期,直接跳过
 21:         if (expiretime < now) return 0;
 22: 
 23:         if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
 24:         if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
 25:     }
 26: 
 27:     /* Save type, key, value */
 28:     // 保存值类型
 29:     if (rdbSaveObjectType(rdb,val) == -1) return -1;
 30:     // 保存 key
 31:     if (rdbSaveStringObject(rdb,key) == -1) return -1;
 32:     // 保存 value
 33:     if (rdbSaveObject(rdb,val) == -1) return -1;
 34: 
 35:     return 1;
 36: }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值