Redis持久化
redis持久化
持久化是为了保证redis数据在单机上的安全性,由于redis是基于内存的服务,在服务重启或宕机时内存数据丢失,需要持久化机制保证不受此影响。
持久化原理
将内存中的数据描述信息写到磁盘中,重启后加载持久化文件,通过记录的描述信息恢复数据,避免数据意外丢失。不同的持久化技术对数据的状态描述方式不同, 也就是生成的持久化文件不同。
RDB
Redis DataBase, redis默认的持久化方式。将某一时刻数据库的所有数据以快照的方式保存到rdb文件。
触发
- 手动触发 save命令 save命令在执行过程中会阻塞redis-server,知道持久化完成。
- 手动触发 bgsave命令 background save, 生成一个子进程,后台运行save,不会阻塞redis-server。
- 自动触发 根据配置文件中设置的条件,当任一条件满足时触发bgsave,自动进行持久化操作。
- 查看持久化时间
lastsave
: 返回上一次持久化的时间戳。
SNAPSHOT配置
-
save point
# Save the DB to disk. # # save <seconds> <changes> [<seconds> <changes> ...] # # Redis will save the DB if the given number of seconds elapsed and it # surpassed the given number of write operations against the DB. # # Snapshotting can be completely disabled with a single empty string argument # as in following example: # # save "" # # Unless specified otherwise, by default Redis will save the DB: # * After 3600 seconds (an hour) if at least 1 change was performed # * After 300 seconds (5 minutes) if at least 100 changes were performed # * After 60 seconds if at least 10000 changes were performed # # You can set these explicitly by uncommenting the following line. # # save 3600 1 300 100 60 10000
save 3600 1 300 100 60 10000
: 1h 1次写操作/ 5min 100次 / 1min 10000次, 满足其一触发bgsave. -
stop-writes-on-bgsave-error 当持久化执行出错时,是否不再进行持久化。 no: 当持久化恢复正常时,继续执行。
-
压缩
rdbcompression yes
: 写rdb文件时是否使用LZF压缩算法。 -
rdb文件校验
rdbchecksum yes
: 对rdb文件进行校验,检查文件是否损坏,保证数据的一致性。序列化和反序列化rdb文件时10%性能损失。 -
安全检测
sanitize-dump-payload no
-
rdb文件名
dbfilename dump.rdb
-
rdb-del-sync-files
rdb-del-sync-files no
: 是否删除持久化文件(只有当AOF和RDB都没有开启的时候才会生效) -
dir rdb、aof文件的生成目录
RDB文件结构
rdb文件总体结构
- REDIS, 固定字符,作为RDB文件的起始标记.
- db_version, 标记rdb的版本信息. 10字符,
REDIS%04d.format(RDB_VERSION)
- EOF,1B,rdb数据内容的结束标记
- check_sum, 根据前面4部分计算得到的校验和 8bit。能够检测出损坏,但是不能保证检测没有问题的时候,文件一定没有损坏。验证时,将EOF,check_sum以外的内容对check_sum取余,不为0判定为损坏。CRC校验算法。
- databases, 数据库内容。
- metas, redis7.0之后新增的kv对信息
kv对信息如下:
/* Save a few default AUX fields with information about the RDB generated. */
int rdbSaveInfoAuxFields(rio *rdb, int rdbflags, rdbSaveInfo *rsi) {
int redis_bits = (sizeof(void*) == 8) ? 64 : 32;
int aof_base = (rdbflags & RDBFLAGS_AOF_PREAMBLE) != 0;
/* Add a few fields about the state when the RDB was created. */
if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"redis-bits",redis_bits) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"ctime",time(NULL)) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"used-mem",zmalloc_used_memory()) == -1) return -1;
/* Handle saving options that generate aux fields. */
if (rsi) {
if (rdbSaveAuxFieldStrInt(rdb,"repl-stream-db",rsi->repl_stream_db)
== -1) return -1;
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid)
== -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset)
== -1) return -1;
}
if (rdbSaveAuxFieldStrInt(rdb, "aof-base", aof_base) == -1) return -1;
return 1;
}
databases结构
databases由若干个databse组成,每个database表示一个数据库的内容。redis7.0中 databse格式如下:
databse中的kv_pair包含若干个键值对信息,每个键值对信息如下(虚线框表示可选):
当然,具体每个value的存储依据不同的数据类型又有着不同的格式,这里不在深入描述。
RDB持久化流程
COW: 父子进程的的虚拟地址指向的是同一块物理地址,只有发生数据写时,触发缺页异常,此时linux会拷贝变动数据到新的内存地址。对于redis主进程, 在rdb期间正常服务,写操作会引起主子进程数据不一致,此时linux内核会为子进程复制一份数据(修改后的内容)。
AOF
RDB 依据配置的save point 阶段性的将数据库内容持久化到rdb文件中,这里存在一个问题,在触发bgsave的间隙,服务宕机/断电等异常 情况下,会丢失部分数据。 Append Only File, 将写操作命令追加到AOF文件中,在恢复时重新执行这些命中以恢复数据. 通过配置,可以保持之多丢失一条/1s数据.
APPEND ONLY MODE 配置
默认情况下,AOF持久化是关闭的。在配置文件中的APPEND ONLU MODE
节中可以配置。
- appendonly
appendonly yes
: 开启AOF持久化
- appendfilename
aof文件名称. redis7.0之后,对应的是一组文件。
- base file: 该文件创建时,数据库的数据快照,可以是rdb/aof格式
- incremental files: 增量数据,记录的是增量的修改命令
- maniftest: 清单文件,记录上述两类的文件的顺序,用于指导恢复数据
- 混合持久化开关 aof-use-rdb-preamble
Redis can create append-only base files in either RDB or AOF formats. Using the RDB format is always faster and more efficient, and disabling it is only supported for backward compatibility purposes.
aof默认情况下开启混合持久化,也就是base file使用rdb格式。
- appenddirname
aof持久化文件输出目录
- appendfsync
aof持久化的执行配置,也就是执行fsync系统调用,将buffer中的数据写入磁盘。有以下几种配置:
- no: redis不干涉,交由操作系统控制落盘。
don’t fsync, just let the OS flush the data when it wants. Faster.
- always: 每一条写操作命令都进行落盘
fsync after every write to the append only log. Slow, Safest
- everysec: 每秒落盘一次. 默认为该设置
fsync only one time every second. Compromise.
- rewrite
auto-aof-rewrite-percentage
: 配置触发重写时aof文件大小变化的阈值
auto-aof-rewrite-min-size
: 配置aof文件触发重写的最小阈值
- rewrite buf落盘阈值
在ADVANCED CONFIG
这个module配置中,有一项配置为aof-rewrite-incremental-fsync
,注释如下:
When a child rewrites the AOF file, if the following option is enabled
the file will be fsync-ed every 4 MB of data generated. This is useful
in order to commit the file to the disk more incrementally and avoid
big latency spikes.
在进行rewrite时,重写结果是暂存到aof_rewrite_buf
中,该buf大小增长超过4MB,则落盘一次,以此避免一次落盘阻塞过长时间。
同理rdb也有一个类似的配置rdb-save-incremental-fsync yes
.
AOF文件结构
Redis协议(aof文件格式)
AOF文件格式也就是redis通信协议格式,aof文件就是依据redis通信协议将命令以文本的形式记录下来。
+
: 表示正确状态的消息-
: 表示错误状态的消息*
: 表示消息体总共有多少行,不包含本行$
: 表示下一行数据的长度:
: 表示返回一个数值
aof示例
AOF rewrite
随着时间推移,aof文件越来越大,会占用大量的磁盘空间,同时恢复数据时时间也会变长,rewriter机制是对aof文件进行压缩。当开启rewrite后, 主进程会fork一个子进程bgrewriteaof执行整理aof文件(rewrite计算),将整理结果写入临时文件,最后对文件重命名,覆盖原文件。
rewrite策略: 1) 读命令不写入 2) 无效命令不写入 3) 过期数据不写入 4) 多条命令合并
启动rewrite
-
手动执行 bgrewriteaof
-
自动执行——配置项
- auto-aof-rewrite-percentage: 当aof文件增大至上次文件的百分之x时触发rewrite
- auto-aof-rewrite-min-size: aof文件执行rewrite操作的最小阈值
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # aof文件重写的最小阈值大小为64M,aof文件大小每新增一倍(大于阈值的情况下)时触发重写。
AOF 落盘策略
同步是指操作系统将buffer中的数据写入到磁盘中,通过调用fsync()实现。
- 交由操作系统控制的这种方式,buffer达到指定大小或间隔一定时间(默认30s),执行一次落盘。执行最快,可能丢失的数据最多。
- everysec, 每秒执行一次落盘,执行速度次之,可能丢失1s的数据
- always,每次写操作都进行落盘,执行速度最慢,可能丢失1条数据
rewrite 期间是否调用fsync()
在同步策略为always/everysec情况下,在子进程执行持久化(bgsave)或aof rewrite(bgrewriteaof)操作时可能会存在大量的IO操作,此时 若是主进程调用fsync()可能会引起较长时间的阻塞。为了解决这个问题,可以通过禁止在rewrite、持久化存在子进程时,禁止执行落盘(相当于此时的 同步策略为’no’,可能存在30s的数据丢失)。
aof 文件截断
aof文件可能被截断当OS发生故障时,此时redis加载时可以直接出错退出或者是尽可能多的加载数据。相关配置:aof-load-truncated yes
当配置设置为yes时,aof截断时,redis会继续加载并通知用户,no时则直接退出。注意这里的截断指的是:aof最后一条命令损坏,此时开启了该设置
则会删除最后一条命令,并加载数据。若是aof文件中间损坏,则需要手动修复aof后再进行加载。
aof 文件修复
redis提供了可执行文件redis-check-aof
修复损坏的aof文件
redis-check-aof aof_file
: 检测指定的aof文件是否损坏
redis-check-aof --fix aof_file
:检测并修复aof文件,对于损坏的命令开始后续命令进行删除
AOF与RDB对比
- rdb
-
优势:rdb文件较小;数据恢复较快;
-
不足:依据save point进行持久化,安全性较差;写时复制影响性能;RDB文件可读性较差
- aof
- 优势:数据安全性高,通过配置同步策略,可能做到只丢失一条数据;aof文件是基于redis协议,可读性高
- 不足:aof文件较大,数据恢复较慢;写操作影响性能(记录的是写操作,大小大于数据本身)
官方推荐使用混合持久化,base file使用rdb,增量数据使用aof;若对数据安全性要求较高,则使用aof, 不推荐单纯使用rdb;仅用于缓存的数据,可以不使用持久化
持久化执行过程
BGSave命令的执行流程在源码src/rdb.c
文件的bgsaveCommand
函数中。
void bgsaveCommand(client *c) {
int schedule = 0;
/* The SCHEDULE option changes the behavior of BGSAVE when an AOF rewrite
* is in progress. Instead of returning an error a BGSAVE gets scheduled. */
if (c->argc > 1) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
if (server.child_type == CHILD_TYPE_RDB) {
addReplyError(c,"Background save already in progress");
} else if (hasActiveChildProcess() || server.in_exec) {
if (schedule || server.in_exec) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"Another child process is active (AOF?): can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenever "
"possible.");
}
} else if (rdbSaveBackground(SLAVE_REQ_NONE,server.rdb_filename,rsiptr) == C_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReplyErrorObject(c,shared.err);
}
}
int rdbSaveBackground(int req, char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;
server.stat_rdb_saves++;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
int retval;
/* Child */
redisSetProcTitle("redis-rdb-bgsave");
redisSetCpuAffinity(server.bgsave_cpulist);
retval = rdbSave(req, filename,rsi);
if (retval == C_OK) {
sendChildCowInfo(CHILD_INFO_TYPE_RDB_COW_SIZE, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
if (childpid == -1) {
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %ld",(long) childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
return C_OK; /* unreached */
}
真正开始写数据的函数:
/* Produces a dump of the database in RDB format sending it to the specified
* Redis I/O channel. On success C_OK is returned, otherwise C_ERR
* is returned and part of the output, or all the output, can be
* missing because of I/O errors.
*
* When the function returns C_ERR and if 'error' is not NULL, the
* integer pointed by 'error' is set to the value of errno just after the I/O
* error. */
int rdbSaveRio(int req, rio *rdb, int *error, int rdbflags, rdbSaveInfo *rsi) {
char magic[10]; // REDIS+rdb_version
uint64_t cksum;
long key_counter = 0;
int j;
if (server.rdb_checksum) // 设置使用校验和的情况下,设置该函数指针
rdb->update_cksum = rioGenericUpdateChecksum;
// 1. 写入 REDIS+rdb_version
snprintf(magic,sizeof(magic),"REDIS%04d",RDB_VERSION);
if (rdbWriteRaw(rdb,magic,9) == -1) goto werr;
// 2. 写入kv信息
if (rdbSaveInfoAuxFields(rdb,rdbflags,rsi) == -1) goto werr;
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA) && rdbSaveModulesAux(rdb, REDISMODULE_AUX_BEFORE_RDB) == -1) goto werr;
/* save functions */
if (!(req & SLAVE_REQ_RDB_EXCLUDE_FUNCTIONS) && rdbSaveFunctions(rdb) == -1) goto werr;
/* save all databases, skip this if we're in functions-only mode */
// 3.写入每个数据库的数据
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA)) {
for (j = 0; j < server.dbnum; j++) {
if (rdbSaveDb(rdb, j, rdbflags, &key_counter) == -1) goto werr;
}
}
if (!(req & SLAVE_REQ_RDB_EXCLUDE_DATA) && rdbSaveModulesAux(rdb, REDISMODULE_AUX_AFTER_RDB) == -1) goto werr;
/* EOF opcode */
// 4. 写入EOF
if (rdbSaveType(rdb,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;
// 5. 写入check_sum
memrev64ifbe(&cksum);
if (rioWrite(rdb,&cksum,8) == 0) goto werr;
return C_OK;
werr:
if (error) *error = errno;
return C_ERR;
}