Redis深度历险 - 持久化实现
Redis支持两种持久化的方式:快照和AOF;快照指的是会将当前数据库中所有的数据记录下来,是一次性全量备份;AOF日志记录的是内存数据修改的指令文本记录,是连续的增量备份。
Redis - 快照
快照是一次性的全量备份,执行
bgsave
命令就会开始全量备份,在此过程中并不会阻塞Redis的正常使用;从这一点上来看,很容易猜到其底层应该是fork了一个新的进程来实现的。
快照命令
docker run -d -p 6379:6379 --name=redis redis
docker exec -it redis /bin/bash

最终会在目录下生成dump.rdb,dump.rdb中的内容并非是明文的
fork的原理
  `fork`是Linux中用来创建子进程的函数,在历史版本中`fork`创建子进程时会对父进程做一份复制操作,即子进程拥有父进程完全一样的数据,Redis正是利用此原理实现的,子进程拥有所有的数据进行备份操作,父进程依然执行业务。
在高版本中的fork
中实现了COW
的特性,即写时复制,过去的版本中此特性由vfork实现;在Redis环境中,写时复制的含义是:子进程创建完毕后,父子进程共享一片内存空间,数据段在操作系统中由很多页面组成,当父进程对某个页面进行操作写入时会先复制一份分离出来进行修改。
快照原理
void bgsaveCommand(client *c)
................
else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
addReplyStatus(c,"Background saving started");
................
createStringConfig("dbfilename", NULL, MODIFIABLE_CONFIG, ALLOW_EMPTY_STRING, server.rdb_filename, "dump.rdb", isValidDBfilename, NULL),
默认情况下备份文件名就是dump.rdb。
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi)
..................
openChildInfoPipe();
if ((childpid = redisFork()) == 0) {
....
retval = rdbSave(filename,rsi);
....
.................
int redisFork() {
int childpid;
long long start = ustime();
if ((childpid = fork()) == 0) {
/* Child */
closeListeningSockets(0); //子进程关闭不需要的监听句柄
setupChildSignalHandlers(); //创建信号将领
} else {
/* Parent */
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
if (childpid == -1) {
return -1;
}
updateDictResizePolicy(); //避免大量的内存页面的复制,所以禁止hash表的rehash操作
}
return childpid;
}
开始执行快照时先进行创建管道负责后续的通信,之后就执行fork
操作。父进程在执行快照的时候会禁止dict
的rehash
操作,但是这段代码好像有问题,实际上无法生效。
Redis
的快照后续的存储并没有涉及到非常复杂的算法技术等,但是也并非文本协议存储的;比如存储时会记录数据的类型,在Redis
中以宏定义表示,存储时并没有说转成字符串表示,以文本方式打开的话可能会看到乱码之类的。
快照相关配置
-
rdbcompression yes
:rdb文件是否压缩 -
Rdbchecksum yes
:恢复数据时是否校验完整性,实质上时进行了一些crc64
计算;每次进行写入文件时,将上一次crc64
的值和此次写入的数据进行crc64
计算 -
dbfilename dump.rdb
:到处文件名 -
dir ./
:存放路径
AOF存储
AOF存储的是Redis服务器的顺序指令记录,记录对内存有修改的记录;如果需要恢复Redis数据,只需要对所有的执行进行重放就可以恢复了
AOF配置
-
appendonly yes
:是否开启AOF持久化 -
appendfilename appendonly.aof
:文件名 -
appendfsync
:磁盘写入策略,主要是因为文件IO有缓冲区可能数据没有真正写入到文件-
always
:每次写入磁盘,性能较差 -
everysec
:每秒写入一次 -
no
:系统处理缓存回写
-
AOF的效果
*2
$6
SELECT
$1
0
*3
$3
set
$4
name
$8
在指定的文件中可以看到AOF是以文本的形式存储的操作记录,这部分基本与Redis的文本传输协议是类似的。
AOF的实现
createBoolConfig("appendonly", NULL, MODIFIABLE_CONFIG, server.aof_enabled, 0, NULL, updateAppendonly),
.........
void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,
int flags)
{
if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)
feedAppendOnlyFile(cmd,dbid,argv,argc);
if (flags & PROPAGATE_REPL)
replicationFeedSlaves(server.slaves,dbid,argv,argc);
}
首先读取配置文件appendonly
决定是否开启,服务器执行完写命令就会调用propagate
继续追加记录
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty();
robj *tmpargv[3];
//如果数据库进行了切换就会记录SELECT命令,Redis默认是有16个数据库的
if (dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
server.aof_selected_db = dictid;
}
.................
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
if (server.aof_child_pid != -1)
aofRewriteBufferAppend((unsigned char*)buf,sdslen(buf));
sdsfree(buf);
}
由于Redis有多个数据库db文件, 所有在数据库切换的情况下就需要记录SELECT命令;同时会对命令做一些处理,最终调用aofRewriteBufferAppend
存储内容。
void aofRewriteBufferAppend(unsigned char *s, unsigned long len) {
listNode *ln = listLast(server.aof_rewrite_buf_blocks);
aofrwblock *block = ln ? ln->value : NULL;
while(len) {
/* If we already got at least an allocated block, try appending
* at least some piece into it. */
if (block) {
unsigned long thislen = (block->free < len) ? block->free : len;
if (thislen) { /* The current block is not already full. */
memcpy(block->buf+block->used, s, thislen);
block->used += thislen;
block->free -= thislen;
s += thislen;
len -= thislen;
}
}
if (len) { /* First block to allocate, or need another block. */
int numblocks;
block = zmalloc(sizeof(*block));
block->free = AOF_RW_BUF_BLOCK_SIZE;
block->used = 0;
listAddNodeTail(server.aof_rewrite_buf_blocks,block);
/* Log every time we cross more 10 or 100 blocks, respectively
* as a notice or warning. */
numblocks = listLength(server.aof_rewrite_buf_blocks);
if (((numblocks+1) % 10) == 0) {
int level = ((numblocks+1) % 100) == 0 ? LL_WARNING :
LL_NOTICE;
serverLog(level,"Background AOF buffer size: %lu MB",
aofRewriteBufferSize()/(1024*1024));
}
}
}
/* Install a file event to send data to the rewrite child if there is
* not one already. */
if (aeGetFileEvents(server.el,server.aof_pipe_write_data_to_child) == 0) {
aeCreateFileEvent(server.el, server.aof_pipe_write_data_to_child,
AE_WRITABLE, aofChildWriteDiffData, NULL);
}
}
可以看到数据并非立刻存储起来的,而是放在一个链表当中存储起来;同时记录注册写事件,等待可写。
AOF的优缺点
- AOF随着进程的执行,整个文件会越来越庞大;需要进行重写瘦身
- AOF在恢复时需要重新执行一边所有的命令,耗时较长
总结
AOF和RDB各有各的长处,在实际部署时也非一成不变的;一种很常用的方法就是同时进行RDB和AOF记录,RDB记录镜像,AOF记录从记录镜像开始的日志,这样双方的优势都可以利用起来。
网友评论