Redis源码阅读 (备份机制)

5. 备份机制

Redis是内存数据库,支持两种方式——snapshot和aof,将数据从内存dump到磁盘。 snapshot是快照式备份,每次将Redis全库dump到磁盘, aof是流水式备份,每次将Redis数据库的修改日志dump到磁盘,并定期整理日志。

5.1. Snapshot

Redis支持Snapshot(快照)式备份。 Redis可以自动检测备份时机,设置每N秒检查,若发生M次以上数据更新操作,则开始Snapshot备份,也可以由客户端发送save或bgsave命令手动启动备份。save是阻塞式备份,备份过程中Redis会停止服务; bgsave是后台备份, Redis将新建一个备份进程负责将全库dump到磁盘。saveCommand()中,首先检查是否有bgsave的后台备份进程,若没有,则执行rdbSave()进行全库备份。bgsaveCommand()中,也会检查是否有bgsave的后台备份进程,若没有,则执行rdbSaveBackground(),启动备份子进程,在后台全库备份。备份子进程最终也是通过rdbSave()备份数据库。rdbSaveBackground()中,首先通过前文所说的waitEmptyIOJobsQueue()检查是否有vm的IO线程在进行数据交换,如果有则阻塞,等待所有vm的IO线程完成工作。然后fork()备份子进程。在父进程中, server.bgsavechildpid记录了备份子进程的pid,并通过updateDictResizePolicy()禁止Hash表自动扩容。在子进程中,如果启用了vm,则打开vm swap文件,然后调用rdbSave()执行备份。因为之前已经检查了所有vm的IO线程是否完成,因此这里打开vm的swap文件并不会和vm的IO线程冲突。

int rdbSaveBackground(char *filename) {

pid_t childpid;

if (server.bgsavechildpid != -1) return REDIS_ERR;

if (server.vm_enabled) waitEmptyIOJobsQueue();

server.dirty_before_bgsave = server.dirty;

if ((childpid = fork()) == 0) {

/* Child */

if (server.vm_enabled) vmReopenSwapFile();

if (server.ipfd > 0) close(server.ipfd);

if (server.sofd > 0) close(server.sofd);

if (rdbSave(filename) == REDIS_OK) {

_exit(0);

} else {

_exit(1);

}

} else {

/* Parent */

if (childpid == -1) {

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.bgsavechildpid = childpid;

updateDictResizePolicy();

return REDIS_OK;

}

return REDIS_OK; /* unreached */

}

rdbSave()中首先通过waitEmptyIOJobsQueue()检查vm的IO线程,并阻塞直到所有vm的IO线程完成。因为备份过程中,需要备份vm的swap文件,如果有未完成的vm的IO线程,会导致数据不同步。首先建立一个tmp文件,向它写入备份数据。依次循环每个db: server.db,server.db+1, ..., server.db+server.dbnum,通过dict的迭代器dictIterator访问db中的所有数据,依次写入tmp文件。Redis在写备份文件时做了许多优化,例如rdbSaveLen()用于写入长度信息,写入的数字不超过32bit整数。 Redis将长度数字按二进制长度分三类:小于6bit, 6bit至14bit, 14bit至32bit,并用前缀REDIS_RDB_6BITLEN(00)、REDIS_RDB_14BITLEN(01)、 REDIS_RDB_32BITLEN(10)标示数字长度。将前缀与数字一起写入磁盘。这个优化类似于Protobuf里压缩数字的方法。类似的压缩还有很多,再此不一一分析。

int rdbSaveLen(FILE *fp, uint32_t len) {

unsigned char buf[2];

int nwritten;

if (len < (1<<6)) {

/* Save a 6 bit len */

buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);

if (rdbWriteRaw(fp,buf,1) == -1) return -1;

nwritten = 1;

} else if (len < (1<<14)) {

/* Save a 14 bit len */

buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6);

buf[1] = len&0xFF;

if (rdbWriteRaw(fp,buf,2) == -1) return -1;

nwritten = 2;

} else {

/* Save a 32 bit len */

buf[0] = (REDIS_RDB_32BITLEN<<6);

if (rdbWriteRaw(fp,buf,1) == -1) return -1;

len = htonl(len);

if (rdbWriteRaw(fp,&len,4) == -1) return -1;

nwritten = 1+4;

}

return nwritten;

}

在备份文件中, Redis写入type/key/value数据,并放弃所有过期的数据。特别地,对于vm中的数据,会先读入内存,再将数据写入备份文件。父进程在serverCron中检查备份进程,如果存在bgsave进程或者bgrewrite进程(AOF备份,下节分析),则wait3()等待进程完成,完成后调用backgroundSaveDoneHandler()或者backgroundRewriteDoneHandler()处理。snapshot式备份中, Redis会停止vm的换入换出操作,停更新LRU策略中的标记信息,但不会影响Redis对内存中的数据的读写。 Redis通过调整snapshot备份的粒度,适应各种应用需求。

5.2. AOF

除了Snapshot备份模式外Redis还支持AOF(流水式)备份,它保存所有操作的commit log,并能自动地进行全库备份和删除无用的commit log。Redis在写commit log时, Redis并不是每次处理请求时都将请求写入磁盘,它会用一段内存Buffer缓存commit log,并在一定时间将缓存中的内容统一写入磁盘。 为了避免磁盘缓存的影响,可以设置三种策略进行fsync():每次写操作后均调用fsync(),每秒调用一次fsync(),从不调用fsync(),三种不同的策略获得不同的性能和一致性。

Redis将所有操作保存在commit log中, commit log的文件是追加写。当commitlog的大小无法承受时,可以手工通过bgrewriteaof命令产生一个快照(这个不同于Snapshot备份),具体构造方式后文分析。

AOF有四种操作:

•feedAppendOnlyFile()-将命令写入AOF。该方法在call()中被调用,用于将

所有Redis处理的命令写入AOF;该方法在Redis发现过期的Keys被调用,记录清除过期Key的操作

call()是Redis中所有命令处理的入口,当启用了AOF且数据有变化时

(server.dirty有变化),将命令用feedAppendOnlyFile()写入AOF,该函数中,将命令编码后写入server.aofbuf,当后台rewriteaof进程存在时,同时将编码后的命令写入server.bgrewritebuf, bgrewritebuf的作用后文解释。

/* Append to the AOF buffer. This will be flushed on disk just before

* of re-entering the event loop, so before the client will get a

* positive reply about the operation performed. */

server.aofbuf = sdscatlen(server.aofbuf,buf,sdslen(buf));

/* If a background append only file rewriting is in progress we want to

* accumulate the differences between the child DB and the current one

* in a buffer, so that when the child process will do its work we

* can append the differences to the new append only file. */

if (server.bgrewritechildpid != -1)

server.bgrewritebuf = sdscatlen(server.bgrewritebuf,buf,sdslen(buf));

•rewriteAppendOnlyFile()-产生快照,并更新aof文件。

rewriteAppendOnlyFile()只在 rewriteAppendOnlyFileBackground()中

被调用。 Redis产生AOF快照主要分为以下几步:

(1) fork()一个rewrite子进程,调用rewriteAppendOnlyFile()产生快照。

(2) 当rewrite进程开始后, rewriteAppendOnlyFile()扫描数据库中所有数

,将他们编码后写入临时文件。 AOF文件的编码形式为文本,即人肉可读的编码。

(3)rewrite时父进程照常接收请求,并将流水日志写入

server.bgrewriteaofbuf中。

(4)子进程完成工作,父进程在serverCron()中通过wait3()获知其状态后,调用

backgroundRewriteDoneHandler()将server.bgrewriteaofbuf中的内容作为新

的commit log,覆盖原有server.aofbuf。

(5) 父进程将rewrite子进程生成的临时文件改名,作为新的aof文件。用于未来恢

复数据。

Redis的AOF快照机制的基础,是数据库的大小一定小于操作日志的大小,否则产生

的快照文件可能远远超过commit log文件的大小。因此,此处有一点值得考虑,

rewrite过程是否可以只写入aofbuf中改变了的数据的快照?

•flushAppendOnlyFile()-将内存buffer中的commit log写到磁盘上。

Redis在beforeSleep()中,及关闭AOF功能时,将内存buffer中的log写入磁盘。

•loadAppendOnlyFile()-重放AOF文件。该方法在Redis启动时被调用,从AOF

文件中重建数据库。

Redis通过AOF重建时,构造一个虚拟的client(),向自己发送重建的命令。而恢复

数据只需要将AOF快照重新载入数据库,并回放commit log中的操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值