简介 redis 持久化 RDB、AOF

redis 提供两种持久化方式:RDB 和 AOF。redis 允许两者结合,也允许两者同时关闭。

  • RDB 可以定时备份内存中的数据集。服务器启动的时候,可以从 RDB 文件中回复数据集。
  • AOF 可以记录服务器的所有写操作。在服务器重新启动的时候,会把所有的写操作重新执行一遍,从而实现数据备份。当写操作集过大(比原有的数据集还大),redis 会重写写操作集。

本篇主要讲的是 RDB 持久化,了解 RDB 的数据保存结构和运作机制。redis 主要在 rdb.h 和 rdb.c 两个文件中实现 RDB 的操作。

数据结构 rio

持久化的 IO 操作在 rio.h 和 rio.c 中实现,核心数据结构是 struct rio。RDB 中的几乎每一个函数都带有 rio 参数。struct rio 既适用于文件,又适用于内存缓存,从 struct rio 的实现可见一斑。

struct _rio {
// 函数指针,包括读操作,写操作和文件指针移动操作
/* Backend functions.
* Since this functions do not tolerate short writes or reads the return
* value is simplified to: zero on error, non zero on complete success. */
size_t (*read)(struct _rio *, void *buf, size_t len);
size_t (*write)(struct _rio *, const void *buf, size_t len);
off_t (*tell)(struct _rio *); // 校验和计算函数
/* The update_cksum method if not NULL is used to compute the checksum of
* all the data that was read or written so far. The method should be
* designed so that can be called with the current checksum, and the buf
* and len fields pointing to the new block of data to add to the checksum
* computation. */
void (*update_cksum)(struct _rio *, const void *buf, size_t len); // 校验和
/* The current checksum */
uint64_t cksum; // 已经读取或者写入的字符数
/* number of bytes read or written */
size_t processed_bytes; // 每次最多能处理的字符数
/* maximum single read or write chunk size */
size_t max_processing_chunk; // 可以是一个内存总的字符串,也可以是一个文件描述符
/* Backend-specific vars. */
union {
struct {
sds ptr;
// 偏移量
off_t pos;
} buffer;
struct {
FILE *fp;
// 偏移量
off_t buffered; /* Bytes written since last fsync. */
off_t autosync; /* fsync after 'autosync' bytes written. */
} file;
} io;
}; typedef struct _rio rio;

redis 定义两个 struct rio,分别是 rioFileIO 和 rioBufferIO,前者用于内存缓存,后者用于文件 IO:

// 适用于内存缓存
static const rio rioBufferIO = {
rioBufferRead,
rioBufferWrite,
rioBufferTell,
NULL, /* update_checksum */
0, /* current checksum */
0, /* bytes read or written */
0, /* read/write chunk size */
{ { NULL, 0 } } /* union for io-specific vars */
}; // 适用于文件 IO
static const rio rioFileIO = {
rioFileRead,
rioFileWrite,
rioFileTell,
NULL, /* update_checksum */
0, /* current checksum */
0, /* bytes read or written */
0, /* read/write chunk size */
{ { NULL, 0 } } /* union for io-specific vars */
};

RDB 持久化的运作机制

redis 支持两种方式进行 RDB:当前进程执行和后台执行(BGSAVE)。RDB BGSAVE 策略是 fork 出一个子进程,把内存中的数据集整个 dump 到硬盘上。两个场景举例:

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

这里主要展开的内容是 RDB 持久化操作的写文件过程,读过程和写过程相反。子进程的产生发生在 rdbSaveBackground() 中,真正的 RDB 持久化操作是在 rdbSave(),想要直接进行 RDB 持久化,调用 rdbSave() 即可。

以下主要以代码的方式来展开 RDB 的运作机制:

// 备份主程序
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
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;
} // 初始化 rdb 结构体。rdb 结构体内指定了读写文件的函数,已写/读字符统计等数据
rioInitWithFile(&rdb,fp); if (server.rdb_checksum) // 校验和
rdb.update_cksum = rioGenericUpdateChecksum; // 先写入版本号
snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr; for (j = 0; j < server.dbnum; j++) {
// server 中保存的数据
redisDb *db = server.db+j; // 字典
dict *d = db->dict;
if (dictSize(d) == 0) continue; // 字典迭代器
di = dictGetSafeIterator(d);
if (!di) {
fclose(fp);
return REDIS_ERR;
} // 写入 RDB 操作码
/* Write the SELECT DB opcode */
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 封装在 robj 里
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. */ // RDB 结束码
/* EOF opcode */
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. */
cksum = rdb.cksum;
memrev64ifbe(&cksum);
rioWrite(&rdb,&cksum,8); // 同步到磁盘
/* Make sure data will not remain on the OS's output buffers */
fflush(fp);
fsync(fileno(fp));
fclose(fp); // 修改临时文件名为指定文件名
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
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; // 记录成功执行保存的时间
server.lastsave = time(NULL); // 记录执行的结果状态为成功
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;
} // bgsaveCommand(),serverCron(),syncCommand(),updateSlavesWaitingBgsave() 会调用 rdbSaveBackground()
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start; // 已经有后台程序了,拒绝再次执行
if (server.rdb_child_pid != -1) return REDIS_ERR; server.dirty_before_bgsave = server.dirty; // 记录这次尝试执行持久化操作的时间
server.lastbgsave_try = time(NULL); start = ustime();
if ((childpid = fork()) == 0) {
int retval; // 取消监听
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave"); // 执行备份主程序
retval = rdbSave(filename); // 脏数据,其实就是子进程所消耗的内存大小
if (retval == REDIS_OK) {
// 获取脏数据大小
size_t private_dirty = zmalloc_get_private_dirty(); // 记录脏数据
if (private_dirty) {
redisLog(REDIS_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
} // 退出子进程
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
/* Parent */
// 计算 fork 消耗的时间
server.stat_fork_time = ustime()-start; // fork 出错
if (childpid == -1) {
// 记录执行的结果状态为失败
server.lastbgsave_status = REDIS_ERR;
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid); // 记录保存的起始时间
server.rdb_save_time_start = time(NULL); // 子进程 ID
server.rdb_child_pid = childpid;
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}

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

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

RDB 数据的组织方式

RDB 的文件组织方式为:数据集序号1:操作码:数据1:结束码:校验和----数据集序号2:操作码:数据2:结束码:校验和......

其中,数据的组织方式为:过期时间:数据类型:键:值,即 TVL(type,length,value)。

举两个字符串存储的例子,其他的大概都以至于的形式来组织数据:

可见,RDB 持久化的结果是一个非常紧凑的文件,几乎每一位都是有用的信息。如果对 redis RDB 数据组织方式的细则感兴趣,可以参看 rdb.h 和 rdb.c 两个文件的实现。

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

int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
long long expiretime, long long now)
{
// 过期时间
/* Save the expire time */
if (expiretime != -1) {
/* If this key is already expired skip it */
if (expiretime < now) return 0;
if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
} /* Save type, key, value */
// 数据类型
if (rdbSaveObjectType(rdb,val) == -1) return -1; // 键
if (rdbSaveStringObject(rdb,key) == -1) return -1; // 值
if (rdbSaveObject(rdb,val) == -1) return -1;
return 1;
}

如果对 redis RDB 数据格式细则感兴趣,欢迎访问我的 github & 欢迎讨论。

参考文档

http://redis.io/topics/persistence

----

捣乱 2014-3-26

http://daoluan.net

深入剖析 redis RDB 持久化策略的更多相关文章

  1. 深入剖析 redis AOF 持久化策略

    本篇主要讲的是 AOF 持久化,了解 AOF 的数据组织方式和运作机制.redis 主要在 aof.c 中实现 AOF 的操作. 数据结构 rio redis AOF 持久化同样借助了 struct ...

  2. Redis的持久化策略

    Redis 持久化: 提供了多种不同级别的持久化方式:一种是RDB,另一种是AOF. RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot). AO ...

  3. 详解Redis RDB持久化、AOF持久化

    1.持久化 1.1 持久化简介 持久化(Persistence),持久化是将程序数据在持久状态和瞬时状态间转换的机制,即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘). 1.2 red ...

  4. 《面试官之你说我听》:简明的图解Redis RDB持久化、AOF持久化

    欢迎关注文章这一系列,一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇> <实战演练,拒绝996篇> 如果此文对你有帮助.喜欢的话,那就点个赞呗,点个关注 ...

  5. 关于redis的持久化策略

    Redis的持久化 Redis虽然是基于内存的存储系统,但是它本身是支持内存数据的持久化的,而且提供两种主要的持久化策略:RDB快照和AOF日志. Redis的RDB快照 Redis支持将当前数据的快 ...

  6. 深入剖析Redis RDN持久化机制

    rdb是redis保存内存数据到磁盘数据的其中一种方式(另一种是AOF).Rdb的主要原理就是在某个时间点把内存中的所有数据的快照保存一份到磁盘上.在条件达到时通过fork一个子进程把内存中的数据写到 ...

  7. Ubuntu redis 实战 持久化策略 主从复制 以及 故障恢复

    推荐文章 redis数据结构学习 redis持久化 redis主从复制 redis哨兵

  8. 深入剖析 redis 主从复制

    主从概述 redis 支持 master-slave(主从)模式,redis server 可以设置为另一个 redis server 的主机(从机),从机定期从主机拿数据.特殊的,一个 从机同样可以 ...

  9. [转载] 深入剖析 redis 主从复制

    转载自http://www.cnblogs.com/daoluanxiaozi/p/3724299.html 主从概述 redis 支持 master-slave(主从)模式,redis server ...

随机推荐

  1. Subsets 子集系列问题 leetcode

    子集系列问题: Coding 问题中有时会出现这样的问题:给定一个集合,求出这个集合所有的子集(所谓子集,就是包含原集合中的一部分元素的集合). 或者求出满足一定要求的子集,比如子集中元素总和为定值, ...

  2. QQ摄像头读取条码

    跟我学机器视觉-HALCON学习例程中文详解-QQ摄像头读取条码 第一步:插入QQ摄像头,安装好驱动(有的可能免驱动) 第二步:打开HDevelop,点击助手—打开新的Image Acquisitio ...

  3. OFM管理

    OMF:oracle management files 作用:不用指定文件的路径大小名字 OMF管理数据文件:db_create_file_dest 传统方式:SQL>create tables ...

  4. 记录ASP.NET Web API 服务接口响应时间

    实现起来很简单,一个Filter就可以搞定!!! /// <summary> /// 监控接口执行时间 /// </summary> public class TimingAc ...

  5. 基于Qt的流程设计器(一)

    一: 先来看一下界面的截图:   说明: 拖动节点的时候,与该节点相关的箭头连线也会跟着调整: 用户可以使用鼠标从一个节点拖出一个箭头到另一个节点(鼠标在空白区域点击一下,拖出的箭头消失)   这三个 ...

  6. [stm32] MPU6050 HMC5883 Kalman 融合算法移植

    一.卡尔曼滤波九轴融合算法stm32尝试 1.Kalman滤波文件[.h已经封装为结构体] /* Copyright (C) 2012 Kristian Lauszus, TKJ Electronic ...

  7. Html 字体大小单位 px em pt

    网页上定义字体大小有常见三种单位,px.em.pt px px是pixel缩写,是基于像素的单位.在浏览网页过程中,屏幕上的文字.图片等会随屏幕的分辨率变化而变化,一个100px宽度大小的图片,在80 ...

  8. Java程序员的日常 —— 工作一天的收获

    看题目可能是扯皮,其实还是有很多专业知识的.从最开始没有注意到设计原则,到后面的jquery实战技巧,都是今天一天碰到的问题. 每天整理一点点,每天收获一点点. 关于软件设计 在设计系统结构的时候,一 ...

  9. 《.NET 编程结构》专题汇总(C#)

    前言     掌握一门技术,首要的是掌握其基础.     笔者从事.NET相关开发多年,也非常喜欢.NET,多年来也积累了很多相关的资料,在此将一些基础性的知识整理成专题,分享之.   导航 基础编程 ...

  10. iOS之富文本

    之前做项目时遇到一个问题: 使用UITextView显示一段电影的简介,由于字数比较多,所以字体设置的很小,行间距和段间距也很小,一大段文字挤在一起看起来很别扭,想要把行间距调大,结果在XCode中查 ...