Redis作为一款内存数据库,它将数据存储在内存里面,是存在Redis进程由于某种原因推出导致数据丢失的风险的,因此了解Redis数据的持久化是很有必要,Redis提供两种持久化方式,分别是AOF和RDB,下面将着重介绍这两种持久化方式
RDB持久化
这种方式是指在指定的时间内将内存中的数据集快照写进磁盘,RDB持久化产生的RDB文件是一个经过压缩的二进制文件,这个文件被保存在磁盘中,Redis可通过该文件恢复数据库当时的状态。Redis提供两个命令给我们进行触发RDB持久化,一个是save,当我们执行save命令,Redis进程将会阻塞进行RDB持久化过程,期间不执行其他的命令请求。另一个是bgsave,这个命令可以在不阻塞服务器进程的情况下执行(通过fork一个子进程执行)。
我们可以在redis.conf配置文件中配置RDB自动间歇性保存数据的策略,譬如:
上图红框里面的三个条件只要满足其中任意一个,都会触发服务器执行bgsave命令:
- 服务器在900秒内,对数据库进行了至少1次修改
- 服务器在300秒内,对数据库进行了至少10次修改
- 服务器在60秒内,对数据库进行了至少10000次修改
RDB保存数据
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 如果 BGSAVE 已经在执行,那么出错
if (server.rdb_child_pid != -1) return REDIS_ERR;
// 记录 BGSAVE 执行前的数据库被修改次数
server.dirty_before_bgsave = server.dirty;
// 最近一次尝试执行 BGSAVE 的时间
server.lastbgsave_try = time(NULL);
// fork() 开始前的时间,记录 fork() 返回耗时用
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
/* Child */
// 关闭网络连接 fd
closeListeningSockets(0);
// 设置进程的标题,方便识别
redisSetProcTitle("redis-rdb-bgsave");
// 执行保存操作
retval = rdbSave(filename);
// 打印 copy-on-write 时使用的内存数
if (retval == REDIS_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
redisLog(REDIS_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
// 向父进程发送信号
exitFromChild((retval == REDIS_OK) ? 0 : 1);
} else {
/* Parent */
// 计算 fork() 执行的时间
server.stat_fork_time = ustime()-start;
// 如果 fork() 出错,那么报告错误
if (childpid == -1) {
server.lastbgsave_status = REDIS_ERR;
redisLog(REDIS_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return REDIS_ERR;
}
// 打印 BGSAVE 开始的日志
redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);
// 记录数据库开始 BGSAVE 的时间
server.rdb_save_time_start = time(NULL);
// 记录负责执行 BGSAVE 的子进程 ID
server.rdb_child_pid = childpid;
// 关闭自动 rehash
updateDictResizePolicy();
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
上面是Redis源码 RDB bgsave保存数据的一个方法,可以看到,if ((childpid = fork()) == 0)这行代码就是fork一个子进程,调用fork()函数会创建一个和父线程几乎一模一样的子线程(只有少数值与原来的进程的值不同),fork()函数实际会返回两个值,如果线程是子线程,返回值是0,如果是父线程返回值是子线程的进程ID。从源码上看Redis RDB bgsave方式就是依靠fork出来的子线程来进行保存数据的操作,保存数据完毕后会通知父进程。
RDB持久化恢复数据过程
下面是3.0版本RedisRDB持久化恢复数据的流程图
优劣势
优势:
- 通过fork子进程后台执行的方式进行持久化,父进程不需要再做其他IO操作,能较大优化Redis性能
- RDB文件都是比较紧凑的,恢复也比较快
劣势:
- 由上面的叙述知道RDB持久化方式一般是自动间歇性的,存在较大的丢失数据风险
- RDB持久化由于是fork子进程的方式,如果要保存的数据集很大,fork的过程耗时较长(保存在内存的数据集要复制一份),可能会导致整个服务器停止服务几百毫秒,设置1秒。
AOF持久化
AOF,Append Only File,它的意思是以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。可以说AOF和RDB的最大区别就是,AOF每次执行是追加变更数据库状态的操作日志到文件中,而RDB是把目前的数据库所有数据全部dump到文件中,是一种快照的方式。
AOF通过appendfsync指定,默认为everysec:
- always: 将缓存区的内容总是即时写到AOF文件
- everysec: 每隔一秒就将缓存区的内容写到AOF文件
- no: 写入AOF文件中的操作由操作系统决定,一般而已,为了提高效率会等待缓存区被填满,才会开始同步数据到磁盘
AOF重写
上面提到,AOF持久化方式会根据appendfsync配置,不断的执行追加更新数据库状态操作日志到文件中,这样随着时间推移,AOF文件会越来越大,有大量的冗余操作日志,比如,set a 100,set a 200,set a 100,这三条命令在AOF文件中恢复数据要执行三次,实际上执行一次set a 100即可。这个时候,Redis利用AOF重写功能,来控制AOF文件大小。AOF重写功能会首先读取数据库中现有的键值对状态,然后根据类型使用一条命令来替代之前的键值对多条命令。
由于AOF重写功能有大量的写操作,Redis在重写时候也是通过fork子进程的方式来执行的,父进程仍然处理新的更新操作请求。这样是可能存在执行重写操作期间,数据不一致的情况,因此Redis还设置了一个AOF重写缓冲区,这个缓冲区在子线程被创建开始之后使用,这个期间,所有的命令会存两份,一份在AOF缓存空间,一份在AOF重写缓冲区,当AOF重写完成之后,子进程会发信号给父进程,通知父线程将AOF重写缓冲区的内容添加到AOF文件中。
优劣势
优势:
- 按照默认配置,最多丢失1秒数据
- 如果日志过大,可以重写控制日志文件大小
- 写入方式是append,没有磁盘寻址开销,性能高
劣势:
- 相同数据集的数据而言AOF文件要远大于RDB文件,恢复速度慢于RDB
- AOF运行效率要慢于RDB,每秒同步策略效率较好,不同步效率和RDB相同
小结
总的来说,AOF,RDB这两种方式各有优劣势,使用哪种方式,要视场景而言,如果要追求很高的数据完备性,推荐用AOF的方式,默认配置最坏情况下只会丢失1秒的数据,如果对丢失数据不是特别敏感,用RDB其实也够了。当然如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式