Redis数据持久化学习笔记
背景介绍
Redis在工作中特别常见,在很多业务架构的分享,Redis常常是作为单纯的缓存使用,目的是缓解持久层(比如MySQL)的大流量的访问,最终起到的作用是防止持久层因为海量访问而挂掉。Redis通过将数据保存在内存中,Redis得以拥有极高的读写性能。一旦服务进程退出,Redis的数据就会全部丢失。所以,很多情况下,业务上并不会使用Redis作为数据存储层。
但是,为了解决这个问题,稍微了解些Redis的同学,至少应该听说过Redis的两个持久化方案,分别是RDB、AOF两种持久化方案,这两个方案目标是一样的,就是将内存中的数据保存到磁盘中,避免数据丢失。很多人和我应该类似,听过但从没在实践中使用Redis持久化数据,在我看来,没用过光听过,肯定不是真正的了解,咱至少也得深入了解(暂时用不上的情况下),不能光停留在“听过”。
我会带着这几个问题,去深入了解Redis的持久化机制?
问题一、Redis的持久化机制RDB如何实现?
问题二、Redis的持久化机制AOF如何实现?
问题三、Redis的持久化机制怎么保证数据高可用?
问题四、Redis的持久化机制在哪些业务场景适用?
Redis的持久化机制-RDB
Redis的持久化机制怎么实现,大白话说法,Redis持久化也是把内存数据保存到磁盘上。RDB(Redis Database)是保存内存数据库的快照(SanpShot),而AOF(Append Only File)是保存执行的写操作列表。但这里面的门道是很多的,我们先来了解下RDB的持久化。
举个例子,上图展示了一个包含三个非空数据库的Redis服务器,这三个数据库以及数据库中的键值对就是该服务器的数据库状态。RDB持久化就是生成一个RDB文件,当然是经过压缩的二进制文件,通过该文件可以还原生成RDB文件时对应的数据库状态。
RDB文件是通过SAVE或者BGSAVE命令创建的,代码是rdb.c/rdbSave,有兴趣可以读一下,我这就不展开分析了,SAVE和BGSAVE最终都会调用rdbSave的函数,区别在于调用的方法不同,看名字也能猜出个大概,SAVE是阻塞运行的,而BGSAVE是非阻塞运行的,
SAVE命令调用的方式:
void saveCommand(redisClient *c) {
// BGSAVE 已经在执行中,不能再执行 SAVE
// 否则将产生竞争条件
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
// 执行
if (rdbSave(server.rdb_filename) == REDIS_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
BGSAVE
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
// 如果 BGSAVE 已经在执行,那么出错
if (server.rdb_child_pid != -1) return REDIS_ERR;
……
if ((childpid = fork()) == 0) {
……
// 执行保存操作
retval = rdbSave(filename);
……
// 向父进程发送信号
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 的时间
server.rdb_save_time_start = time(NULL);
// 记录负责执行 BGSAVE 的子进程 ID
server.rdb_child_pid = childpid;
……
return REDIS_OK;
}
return REDIS_OK; /* unreached */
}
代码很明显能看到,不管是SAVE还是BGSAVE,在执行命令期间(生成RDB文件时),如果再发送SAVE或者BGSAVE都是拒绝的,原因是生成RDB文件时,保存的是全量内存数据,所以极可能产生不小的磁盘I/O和CPU算力。
再来讲讲生成RDB文件的触发方式,有两种,一种是发送SAVE和BGSAVE命令。另外一种是自动间隔性保存,举个例子:
save 1000 1
save 300 20
save 60 20000
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
服务器在1000秒之内,对数据库进行了至少1次修改。
服务器在300秒之内,对数据库进行了至少20次修改。
服务器在60秒之内,对数据库进行了至少20000次修改。
那么代码实现上,关键的数据结构是struct saveparam,如下:
struct redisServer {
……
// 记录保存条件的数组
struct saveparam * saveparams;
……
}
那么上面的自动保存间隔参数,在内存中就是这样保存的:
OK,Redis服务器会有一个定时任务的入口redis.c/serverCron,当中就会遍历以上条件是否满足,满足的话,触犯BGSAVE操作。
// 遍历所有保存条件,看是否需要执行 BGSAVE 命令
for (j = 0; j < server.saveparamslen; j++) {
struct saveparam *sp = server.saveparams+j;
/* Save if we reached the given amount of changes,
* the given amount of seconds, and if the latest bgsave was
* successful or if, in case of an error, at least
* REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */
// 检查是否有某个保存条件已经满足了
if (server.dirty >= sp->changes &&
server.unixtime-server.lastsave > sp->seconds &&
(server.unixtime-server.lastbgsave_try >
REDIS_BGSAVE_RETRY_DELAY ||
server.lastbgsave_status == REDIS_OK))
{
redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
// 执行 BGSAVE
rdbSaveBackground(server.rdb_filename);
break;
}
}
这里面还是不少实现细节的,比如生成RDB文件的子进程如何通知主进程,已经完成了生成文件的操作(通过信号),还有异常情况的处理,主进程收到了Kill信号,这时候也需要调用RDB生成文件,优雅退出程序等等。
关于RDB二进制文件的格式,这里就先略过了,不详细记录在学习笔记上了。
Redis的持久化机制-AOF
再来是AOF持久化机制,AOF( append only file )持久化以独立日志文件的方式记录每条写命令,并在 Redis 启动时回放 AOF 文件中的命令以达到恢复数据的目的。由于AOF会以追加的方式记录每一条redis的写命令,因此随着Redis处理的写命令增多,AOF文件也会变得越来越大,命令回放的时间也会增多,为了解决这个问题,为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。在同步到AOF文件前,Redis服务程序会先把命令写入到AOF缓冲区。
在介绍“AOF文件重写”之前,先简单说下,AOF这里的触发机制,这个和RDB不同,首先没有命令手动触发的方式,完全是通过服务器的设置,appendonly和appendfsync,appendonly打开情况下,appendfsync有3个可选值,always、everysecond和no。always服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,并且同步AOF文件,服务器在每个事件循环都要将aof_buf缓冲区中的所有内容写入到AOF文件,至于何时对AOF文件进行同步,则由操作系统控制。
比较妙的是,虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为“AOF文件重写”,但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的。直接上代码不啰嗦:
// 遍历所有数据库
for (j = 0; j < server.dbnum; j++) {
……
/* Iterate this DB writing every entry
*
* 遍历数据库所有键,并通过命令将它们的当前状态(值)记录到新 AOF 文件中
*/
while((de = dictNext(di)) != NULL) {
sds keystr;
robj key, *o;
long long expiretime;
// 取出键
keystr = dictGetKey(de);
// 取出值
o = dictGetVal(de);
initStaticStringObject(key,keystr);
// 取出过期时间
expiretime = getExpire(db,&key);
/* If this key is already expired skip it
*
* 如果键已经过期,那么跳过它,不保存
*/
if (expiretime != -1 && expiretime < now) continue;
/* Save the key and associated value
*
* 根据值的类型,选择适当的命令来保存值
*/
if (o->type == REDIS_STRING) {
/* Emit a SET command */
char cmd[]="*3\r\n$3\r\nSET\r\n";
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
/* Key and value */
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkObject(&aof,o) == 0) goto werr;
} else if (o->type == REDIS_LIST) {
if (rewriteListObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_SET) {
if (rewriteSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_ZSET) {
if (rewriteSortedSetObject(&aof,&key,o) == 0) goto werr;
} else if (o->type == REDIS_HASH) {
if (rewriteHashObject(&aof,&key,o) == 0) goto werr;
} else {
redisPanic("Unknown object type");
}
/* Save the expire time
*
* 保存键的过期时间
*/
if (expiretime != -1) {
char cmd[]="*3\r\n$9\r\nPEXPIREAT\r\n";
// 写入 PEXPIREAT expiretime 命令
if (rioWrite(&aof,cmd,sizeof(cmd)-1) == 0) goto werr;
if (rioWriteBulkObject(&aof,&key) == 0) goto werr;
if (rioWriteBulkLongLong(&aof,expiretime) == 0) goto werr;
}
}
// 释放迭代器
dictReleaseIterator(di);
}
是不是很简单明了,但是AOF这里有一个重写一致性的问题,举例子:
上图所示,当子进程开始进行文件重写时,数据库中只有k1一个键,但是当子进程完成AOF文件重写之后,服务器进程的数据库中已经新设置了k2、k3、k4三个键,因此,重写后的AOF文件和服务器当前的数据库状态并不一致,新的AOF文件只保存了k1一个键的数据,而服务器数据库现在却有k1、k2、k3、k4四个键。为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。如下图所示:
最右侧的虚线表示重写完之后,替换AOF文件。更详细的流程,可以参考下图:
注:AOFRW表示AOF文件重写
Redis的持久化机制怎么保证数据高可用
关于这个问题,在了解上述持久化机制的实现方案后,我的理解是,不管是RDB还是AOF机制,都存在丢失数据的可能性。RDB机制下,数据丢失的概率比AOF机制更大些,而且不管是RDB还是AOF都仅仅是单机上的数据持久化,如果单机的存储磁盘挂了,是没有多备份的,关于这点,Redis也是有解决方案的,Redis的哨兵模式或者Redis集群模式就能提供多机的高可用,所以在实际业务场景中,我认为完全有可能,在清楚地评估业务丢失数据容忍度的情况下,去使用Redis的数据持久化方案。
Redis的持久化机制在哪些业务场景适用
对持久化有要求,又特别适合使用Redis的场景主要有这么两个,特点数据量不大,写入和查询比例差不多。
- 排行榜相关问题
关系型数据库在排行榜方面写入和查询速度较慢,可能不太适合使用关系型数据库,太重。
比如在线上PK类型的活动中,需要实时展示参与作品的点赞排行榜, 点赞数随着活动进行会不断变化,可能还涉及关联点赞用户的基础信息,那么可以使用到redis中的数据结构,SortedSet和hashmap,来组合使用,满足业务场景。
- 好友关系、黑白名单等的存储
在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能,或者黑白名单的存储,都可以使用Redis作为持久存储,写入和查询速度较快,也不会比关系型数据库重,而且可以快速响应需求。