在redis中, RDB主要用来进行数据库的全量备份, AOF主要用来进行增量备份, 他们有不同的使用场景, 在真实的线上环境, 比较常见的是结合全量备份和增量备份来实现按时间点的回复。
RDB备份
RDB是对备份时间点的数据库现有数据的一个snapshot, 它的基本原理是遍历数据库内指定的或者全部库, 将数据按照比较紧凑的方式保存在一份数据里面。这里, 比较紧凑的意思是将数据以特定的格式保存, 并且还可以以LZF压缩的方式存储(需要配置)。
RDB 的备份分为2种方式:自动备份和手动备份。
自动备份是在serverCron里面实现, 每当配置文件中指定的自动备份条件满足的时候, 系统自动进行备份。比如, 在配置里面指定如下的备份条件:
save 900 1
save 300 10
save 60 10000
相关的处理代码如下:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
// RDB 或者AOF 子进程返回值的处理
} else {
// 当如下的条件之一满足后, 调用RDB自动备份
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
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;
}
}
同时, 也看到自动备份的入口为rdbSaveBackground。
手工备份, 其入口函数是:
struct redisCommand redisCommandTable[] = {
{"save",saveCommand,1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
{"bgsave",bgsaveCommand,-1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
}
手工备份,有前台备份和后台备份。
前台备份通过rdbSave来实现, 它是由主进程操作实现, 也就是说,在主进程实现RDB前台备份的过程中, 是无法同时接受客户端的相应的, 显然这种方式的缺点很明显:无法应对高并发的场景, 优点就是处理简单;
后台备份是通过rdbSaveBackground来实现, 通过主进程启动一个新的进程, 该子进程调用rdbSave产生备份文件,主子进程之间通过pipe的方式启动一个无名管道,进行信息的传递, 比如,子进程COW数据的大小。 这种备份的好处是不会影响对外提供服务, 因为整个备份过程是异步操作, 缺点就是处理起来相对复杂一些。
AOF备份
AOF备份数据
AOF是将影响到redis数据库状态的所有操作记录下来并写入到一个指定的AOF文件里面, 当需要的时候, 通过回放AOF文件内的一条条redis命令, 来重新构建内存状态, 达到回复数据库的目的。它非常类似于数据库的操作日志的同步与回放。
redis里面的命令非常多, 他们使用一样的接口来处理, 那些命令需要记录在AOF里, 哪些不需要,是通过在调用call()之前是否是定PROPAGATE_AOF flag来表明该操作是否需要记录到AOF, master-slave之间是否需要同步命令, 使用一样的操作方式。
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);
}
feedAppendOnlyFile里面记录了需要记录在AOF中各种命令的存储格式, 也就是AOF协议格式,以便读取的时候能够得到正确的回放。
每一条命令是如果都要写入AOF文件然后返回客户端, 就会在成太频繁的IO操作, 影响redis的系统性能; 不加以控制由系统决定又无法保证数据的落盘, 因此, redis通过如下的配置项来指定:
appendfsync always/everysec/ no
通常, 我们需要续订everysec, 兼顾性能和数据安全性。
AOF随着时间的推移会越变越大, 回放的时间也就会越来越长, 为了提升回放的效率, redis会定期重写AOF。
AOF重写
AOF重写操作的触发有2中方式:用户触发与后台周期检测触发。
用户触发是由命令bgrewriteaof进行, 对应的入口函数为
bgrewriteaofCommand:
struct redisCommand redisCommandTable[] = {
{"bgrewriteaof",bgrewriteaofCommand,1,
"admin no-script",
0,NULL,0,0,0,0,0,0},
}
后台周期性的检测触发, 它的触发点也在serverCron里面, 当没有RDB或者AOF 重写操作的时候, 系统会自动检查AOF的大小以及相对于上次重写时的AOF大小, 决定是否进行重写操作。如下2个配置项可以指定触发的条件:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
代码的实现如下:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
...
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
// RDB 或者AOF 子进程返回值的处理
} else {
/* If there is not a background saving/rewrite in progress check if
* we have to save/rewrite now. */
/* Trigger an AOF rewrite if needed. */
if (server.aof_state == AOF_ON &&
server.rdb_child_pid == -1 &&
server.aof_child_pid == -1 &&
server.aof_rewrite_perc &&
server.aof_current_size > server.aof_rewrite_min_size)
{
long long base = server.aof_rewrite_base_size ?
server.aof_rewrite_base_size : 1;
long long growth = (server.aof_current_size*100/base) - 100;
if (growth >= server.aof_rewrite_perc) {
serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
rewriteAppendOnlyFileBackground();
}
}
}
...
}
这里的操作跟background RDB备份非常相似, 由主进程启动一个子进程, 由子进程完成相关的重写操作rewriteAppendOnlyFile; 在重写操作进行过程中,主进程新的修改数据库的操作被记录在server.aof_buf里面, 当该buffer满之后, 写入到aof_rewrite_buf_blocks 列表内,同时会产生一个写入操作信号, 通知重写子进程重写期间产生的所有操作; 子进程与父进程的通信也是通过pipe的方式传递增量信息与ack信息:
server.aof_pipe_write_data_to_child = fds[1];
server.aof_pipe_read_data_from_parent = fds[0];
server.aof_pipe_write_ack_to_parent = fds[3];
server.aof_pipe_read_ack_from_child = fds[2];
server.aof_pipe_write_ack_to_child = fds[5];
server.aof_pipe_read_ack_from_parent = fds[4];
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);
}
}
备份数据的加载
在main函数内loadDataFromDisk来将备份数据加载到内存中, 如果AOF存在通过AOF文件加载, 否则通过RDB加载。
void loadDataFromDisk(void) {
long long start = ustime();
if (server.aof_state == AOF_ON) {
if (loadAppendOnlyFile(server.aof_filename) == C_OK)
serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
} else {
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
}
}
}