redis持久化策略
Redis的持久化的策略:
Redis的持久化的策略分成两种:RDB与AOF:
一. RDB持久化的策略:
RDB全程叫作Redis Database Backup file (Redis数据备份文件),也被称之为Redis的数据快照,简单来说就是把内存中的所有的数据都记录到对应的磁盘当中。当Redis实例故障重启以后,从磁盘读取快照文件,恢复相应的数据。
注意:RDB类型的持久化的操作是将数据存储到对应的dump.rdb文件当中:
我们可以在redis.conf配置文件当中配置触发的rdb的触发时机:
save 60 5 #只要1分钟内修改5次就会触发RDB的保存
注意RDB的持久化的策略有以下两种:
- 手动触发:
1.save: 该线程会阻塞当前redis服务器,执行save指令期间,redis不能处理其他的命令,直到RDB持久化完成。如果RDB文件较大,会产生比较大的影响,生产上谨慎使用,因为会阻塞当前Redis的线程使其不能够去完成对于客户端的命令的处理
2.bgsave:为了解决save命令存在的痛点,bgsave会异步执行RDB持久化操作,不会影响redis处理其他的请求。Redis服务器会通过fork操作来创建一个子进程,专门处理此次的RDB持久化操作,完成后自动结束。Redis只会在fork过程中发生阻塞,而且时间较短,当然,fork时间也会随着数据量变大而变长,需要占用的内存也会加倍。
- 自动触发:
就是我们在前面说的可以在redis.conf中配置相应的触发时机,当redis服务器满足触发时机的条件就会触发保存
RDB持久化方式的底层原理:
bgsave开始时会fork主进程得到相应的子进程,子进程共享主进程的内存数据。完成fork后读取内存的数据并写入RDB文件。
其实涉及到的是进程的虚拟内存与实际物理内存的之间的映射关系的问题,这一块是操作系统的知识点,那么不熟悉的首先需要去回顾一下内存分配中的请求页面分配策略与页面置换算法才好理解接下来说的一些术语:
- 注意主进程是操作内存中的数据的,但是这种操作并不是直接操作内存中的数据,主进程只能操作虚拟内存然后虚拟内存再根据页表操作相应的物理内存。
- 注意主进程fork子进程其实是将主进程的页表分配给了子进程。
但是大家有没有想到一个问题,既然说的是子进程与主进程是异步的,那么在异步的过程当中如果主进程向内存中写数据,而子进程读会不会造成数据不一致的情况出现。
fork底层采用的是copy-on-write的技术:
- 当主进程执行读操作的时候,访问共享内存
- 当主进程执行写操作时候,则会拷贝一份数据,执行相应的写操作
RDB中最核心的就是rdbSave与rdbLoad两个函数,rdbSave用于生成RDB文件到磁盘当中,而rdbLoad则用于将RDB文件中的数据重新载入到内存当中,rdbSave将快照写入RDB文件时候,如果文件已经存在,则新的RDB文件会将已经有的文件进行覆盖。
rdbSave函数:可以看到纯内存的操作,C语言中的文件操作,打开一个文件然后写入相应的数据。
rdbLoad:打开一个文件然后操作是读操作:
自动保存的底层实现原理:
redis有一个周期操作函数serverCron,默认是每100毫秒就会执行一次,只要是服务器正在运行,这个函数也就会一直在进行工作,检查save设置的条件是否得到满足,这个函数其中一个功能就是,如果满足,就会执行bgsave。基本所有的RDB操作都是默认使用bgsave而非save就是为了不影响主进程进行对于客户端的命令进行处理,
serverCron底层的源码:(在redis.c文件中)
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
int j;
......
/* 检查正在进行中的后台保存或AOF重写是否已终止。 */
if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
ldbPendingChildren())
{
int statloc;
pid_t pid;
// 查看这个进程是否正常返回信号
if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
int exitcode = WEXITSTATUS(statloc);
int bysignal = 0;
if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
if (pid == -1) {
// 异常处理
serverLog(LL_WARNING,"wait3() returned an error: %s. "
"rdb_child_pid = %d, aof_child_pid = %d",
strerror(errno),
(int) server.rdb_child_pid,
(int) server.aof_child_pid);
} else if (pid == server.rdb_child_pid) {
//成功持久化 RDB 文件,调用方法用心的RDB文件覆盖旧的RDB文件
backgroundSaveDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else if (pid == server.aof_child_pid) {
// 成功执行 AOF,替换现有的 AOF文件
backgroundRewriteDoneHandler(exitcode,bysignal);
if (!bysignal && exitcode == 0) receiveChildInfo();
} else {
if (!ldbRemoveChild(pid)) {
serverLog(LL_WARNING,
"Warning, detected child with unmatched pid: %ld",
(long)pid);
}
}
updateDictResizePolicy();
closeChildInfoPipe();
}
} else {
/* 如果没有后台保存/重写正在进行中,检查是否我们现在必须保存/重写。*/
for (j = 0; j < server.saveparamslen; j++) {
//计算距离上次执行保存操作多少秒
struct saveparam *sp = server.saveparams+j;
/* 如果我们达到了给定的更改量,并且最新的bgsave是成功的,或者在出现错误的情况下,至少CONFIG_BGSAVE_RETRY_DELAY秒已经过去,那么就进行保存。*/
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);
// 启动bgsave
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
......
}
rdbSaveBackGround:
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
long long start;
// 如果已经有aof或者rdb持久化任务,那么child_pid就不是-1,会直接返回
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
//bgsave之前的dirty数保存起来
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
//打开子进程信息管道
openChildInfoPipe();
// 记录RDB开始的时间
start = ustime();
// fork一个子进程
if ((childpid = fork()) == 0) {
//如果fork()的结果childpid为0,即当前进程为fork的子进程,那么接下来调用rdbSave()进程持久化;
int retval;
/* Child */
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
// 真正在子进程里调用rdbsave
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_RDB);
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* 父进程 */
// 更新fork进程需要消耗的时间
server.stat_fork_time = ustime()-start;
// 更新fork速率:G/秒,zmalloc_used_memory()的单位是字节,所以通过除以(1024*1024*1024),得到GB信息,fork_time:fork时间是微妙,所以得乘以1000000,得到每秒钟fork多少GB的速率;
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) {
// fork子进程出错
closeChildInfoPipe();
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 %d",childpid);
// 最后需要记录redisServer中的变量值
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
return C_OK;
}
return C_OK; /* unreached */
}
二. AOF持久化策略:
AOF**(Append only file)
将我们所有命令都记录下来。相当于history文件,恢复时候直接所有的指令都再执行一遍。注意此时记录的指令我们是完全按照Redis通信协议指令进行保存的
以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
Aof保存的是 appendonly.aof 文件
我们需要在redis.conf文件中进行配置:
appendonly yes #默认是不开启的,我们需要手动配置成no来进行开启
其他的一些配置:
开启以后自动就产生了aof文件:
实战操作:可以发现完全按照redis的通信指令进行保存:
重写的规则说明:
因为对于aof来说默认就是文件的无限的追加,所以文件的大小就会越来越大:
如果一个aof文件大于64mb,太大了!fork一个新的进程来将我们的文件进行重写!