Redis支持两种方式的持久化,分别是定时快照(rdb)和语句追加(aof),下面会详细分析这两种持久化方式。
一、定时快照
1、原理
定时快照即rdb(snapshotting),Redis内部定时器事件触发时,检查当前数据发生改变的次数与时间是否满足
配置文件中指定的持久化条件,如果满足则fork出一个子进程来完成快照任务,而主进程任然提供服务,当有写入操作时由系统以内存页(page)为单位进行copy-on-write。
2、流程
(1)save命令
save命令执行一个同步保存操作,将当前Redis实例的所有数据快照以rdb文件的形式保存到磁盘,这个操作会阻塞主线程的工作,通常在生产环境上很少执行save而是执行bgsave来完成快照。收到客户端发送的save命令后,会执行saveCommand(Rdb.c/1160),进而执行rdbSave(Rdb.c/597),rdbSave函数的主脉络如下:
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success */
int rdbSave(char *filename) {
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
...
rioInitWithFile(&rdb,fp);
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
...
for (j = 0; j < server.dbnum; j++) {
di = dictGetSafeIterator(d);
...
/* 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;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr;
}
dictReleaseIterator(di);
}
...
/* EOF opcode */
if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr;
...
/* 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) {
...
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;
...
}
(2)bgsave命令
bgsave命令用于在后台异步快照数据到磁盘,收到该命令后调用bgsaveCommand(Rdb.c/1172)函数进而调用rdbSaveBackground(Rdb.c/685)函数,在该函数中Redis fork(Rdb.c/694)出一个子进程,主进程继续处理客户端请求,而子进程则调用rdbSave(Rdb.c/597)函数来负责完成快照,然后退出,子进程的退出状态由serverCron
(Redis.c/756)调用backgroundSaveDoneHandler(Rdb.c/1138)来判断,具体可参见
这里,也可以看源码。具体处理流程如图1所示。
(3)sync命令
master收到slave发送的sync命令后,调用syncCommand(Replication.c/83),进而调用rdbSaveBackground
(Rdb.c/685)函数以完成快照,具体流程如图1所示。
(4)数据变化
在redis.conf配置文件中如下设置以开启rdb:
save 900 1
save 300 10
save 60 10000
也可以通过命令来达到上面效果,如:
config set save "900 1 300 10 60 10000"
当数据在多少秒内出现了多少次变化则触发一次bgsave,触发规则用如上所示方式配置。触发机制由Redis内部定时检测serverCron(Redis.c/756),具体代码如下:
/* If there is not a background saving/rewrite in progress check if
* we have to save/rewrite now */
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds) {
redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, sp->seconds);
rdbSaveBackground(server.rdb_filename);
break;
}
}
如上代码中rdbSaveBackground是亮点,根据前面分析,接下来的事情,你懂的
,具体流程如图1所示。
(5)flushall命令
收到flushall命令后,调用flushallCommand(Db.c/188)函数,再调用rdbSave
(Rdb.c/597)函数清空rdb数据,以免crash后重新加载数据时载入旧数据。
(6)shutdown命令
收到shutdown命令后,调用shutdownCommand(Db.c/305)函数,再调用prepareFroShutdown(Redis.c/1584)函数,进入调用
rdbSave(Rdb.c/597)函数以在关闭Redis时持久化rdb数据。
图1 快照rdb流程图
二、语句追加
1、原理
语句追加即aof(append-only file)类似于MySQL的binlog方式,每条会使Redis内存数据发生改变的命令都会追加到log文件中以完成持久化。
2、流程
在redis.conf配置文件中如下设置以开启aof:
appendonly yes
在redis.conf配置文件中如下设置以指定从page cache刷新数据到磁盘的策略:
#appendfsync always
appendfsync everysec
#appendfsync no
也可以通过命令来达到上面效果,如:
config set appendonly yes
config set appendfsync everysec
如上配置后,在每次收到并执行命令后如果数据发生变化,会调用函数feedAppendOnlyFile(Aof.c/233),将数据命令写入server.aof_buf
(Aof.c/271),下一次主循环调用before_sleep(Redis.c/915)函数时会通过调用flushAppendOnlyFile(Redis.c/85)函数把server.aof_buf(Aof.c/271)里的数据写到aof文件中,具体流程如下图所示:
图2 追加aof流程图
Redis在crash之后,重新启动会读取aof文件并执行其中的所有命令以完成数据恢复。aof除了影响性能外还有一个比较严重的问题就是随着时间的推移,数据频繁变更,aof文件会变得很大,所以需要执行bgrewriteaof命令来重新整理aof文件,只保留最新的kv数据。bgrewriteaof命令执行aof文件重写操作,重写操作只会在没有其它持久化操作正在进行时才会触发,如果有快照则操作会被预定,等到快照完成后再执行,该函数的返回值会告知OK且带上额外信息以说明这一情况,如果已经有别的aof操作,则会该函数会返回一个错误且不会被预定到下次再执行。具体请参看brrewriteaofCommand(Aof.c/834)函数源码。
三、总结
不持久化会带来高性能,充当纯粹的cache时非常合适,但如果需要持久化的场景,就需要二选一了,定时快照对性能影响相对低,但是在两次快照之间存在数据丢失的风险,语句追加丢失数据的风险取决于持久化策略,但性能也会大打折扣。这之间的平衡是由架构师去考量。