之前的文章介绍了Redis的简略数据结构的相干应用和底层原理,这篇文章咱们就来聊一下Redis应该如何保障高可用。
数据长久化
咱们晓得尽管单机的Redis尽管性能非常的杰出, 单机可能扛住10w的QPS,这是得益于其基于内存的疾速读写操作,那如果某个工夫Redis忽然挂了怎么办?咱们须要一种长久化的机制,来保留内存中的数据,否则数据就会间接失落。
Redis有两种形式来实现数据的长久化,别离是RDB(Redis Database)和AOF(Append Only File),你能够先简略的把RDB了解为某个时刻的Redis内存中的数据快照,而AOF则是所有记录了所有批改内存数据的指令的汇合(也就是Redis指令的汇合),而这两种形式都会生成相应的文件落地到磁盘上,实现数据的长久化,不便下次复原应用。
接下来就别离来聊聊这两种长久化计划。
RDB
在redis中生成RDB快照的形式有两种,一种是应用save
,另一种是bgsave
,然而底层实现上,其调用的是同一个函数,叫rdbsave
,只是其调用的形式不同而已。
生成办法
save
save命令间接调用rdbsave
办法,此时会阻塞Redis主过程,直至快照文件生成。
void saveCommand(client *c) { if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); return; } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (rdbSave(server.rdb_filename,rsiptr) == C_OK) { addReply(c,shared.ok); } else { addReply(c,shared.err); }}
bgsave
bgsave命令会fork出一个子过程,由fork进去的子过程调用rdbsave
。父过程会持续响应来自客户端的读写申请。子过程实现RDB文件生成之后会给父过程发送信号,告诉父过程保留实现。
/* BGSAVE [SCHEDULE] */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 { addReply(c,shared.syntaxerr); return; } } rdbSaveInfo rsi, *rsiptr; rsiptr = rdbPopulateSaveInfo(&rsi); if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); } else if (hasActiveChildProcess()) { if (schedule) { 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(server.rdb_filename,rsiptr) == C_OK) { addReplyStatus(c,"Background saving started"); } else { addReply(c,shared.err); }}
这也就是为什么Redis是单线程的,但却可能在生成RDB文件的同时对外提供服务。fork
是unix零碎上创立过程的次要办法,会把父过程的所有数据拷贝到子过程中,父子过程共享内存空间。
fork之后,操作系统内核会把父过程中的所有内存设置为只读,只有当产生写数据时,会产生页异常中断,内核会把对应的内存页拷贝一份,父子过程各持有一份,所以在生成RDB过程中,因为应用了COW,内存脏页会逐步和子过程离开。
那么有没有可能在调用bgsave
的过程中,我再调用save
命令呢,这个时候岂不是会生成两份RDB文件?
实际上在调用save
命令时,Redis会判断bgsave
是否正在执行,如果正在执行服务器就不能再调用底层的rdbsave
函数了,这样做能够防止两个命令之间呈现资源竞争的状况。
例如,在save
命令中,有如下的判断:
if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress"); return;}
而在bgsave
中又有如下的判断:
if (server.rdb_child_pid != -1) { addReplyError(c,"Background save already in progress");} else if (hasActiveChildProcess()) { ...}
能够看到都是对同一个变量的判断,如下:
pid_t rdb_child_pid; /* PID of RDB saving child */
换句话说,在调用save、bgsave命令的时候,会提前去判断bgsave
是否依然在运行当中,如果在运行当中,则不会继续执行bgsave命令。而save命令自身就是阻塞的,如果此时有其余的命令过去了都会被阻塞, 直到save执行结束,才会去解决。
那我把RDB文件生成了之后怎么应用呢?
Redis在启动服务器的时候会调用rdbLoad
函数,会把生成的RDB文件给加载到内存中来,在载入的期间,每载入1000个键就会解决一次曾经达到的申请,然而只会解决publish、subscribe、psubscribe、unsubscribe、punsubscribe这个五个命令。其余的申请一律返回谬误,直到载入实现。
你吹的这么好,RDB的优缺点别离是啥?
长处
RDB策略能够灵便配置周期,取决于你想要什么样的备份策略。例如:
- 每小时生成一次最近24小时的数据
- 每天生成最近一周的数据
- 每天生成最近一个月的数据
基于这个策略,能够疾速的复原之前某个时间段的数据。
其次,RDB十分的适宜做冷备份,你能够把RDB文件存储后转移到其余的存储介质上。甚至能够做到跨云存储,例如放到OSS上的同时,又放到S3上,跨云存储让数据备份更加的强壮。
而且,基于RDB模式的复原速度比AOF更快,因为AOF是一条一条的Redis指令,RDB则是数据最终的模样。数据量大的话所有AOF指令全副重放要比RDB更慢。
毛病
RDB作为一个数据长久化的计划是可行的,然而如果要通过RDB做到Redis的高可用,RDB就不那么适合了。
因为如果Redis此时还没有来得及将内存中的数据生成RDB文件,就先挂了,那么间隔上次胜利生成RDB文件时新增的这部分数据就会全副失落,而且无奈找回。
而且,如果内存的数据量很大的话,RDB即便是通过fork子过程来做的,然而也须要占用到机器的CPU资源,也可能会产生很多的也异常中断,也可能造成整个Redis进行响应几百毫秒。
AOF
下面提到过RDB不能满足Redis的高可用。因为在某些状况下,会永久性的失落一段时间内的数据,所以咱们来聊聊另一种解决方案AOF。首先咱们得有个概念,那就是RDB是对以后Redis Server中的数据快照,而AOF是对变更指令的记录(所有的获取操作不会记录,因为对以后的Redis数据没有扭转)。
然而也正因为如此,AOF文件要比RDB文件更大。上面聊一下一个Redis命令申请从客户端到AOF文件的过程。
AOF记录过程
首先Redis的客户端和服务器之间须要通信,客户端发送的不是咱们写入的字符串,而是专门的协定文本。如果你能够相熟Thrift或者Protobuf的话应该就能了解这个协定。
例如执行命令 SET KEY VALUE
,传到服务器就变成了"*3\\r\\n$3\\r\\nSET\\r\\n$3\\r\\nKEY\\r\\n$5\\r\\nVALUE\\r\\n"
。
而后Redis服务器就会依据协定文本的内容,抉择适当的handler进行解决。当客户端将指令发送到Redis服务器之后,只有命令胜利执行,就会将这个命令流传到AOF程序中。
留神,流传到AOF程序中之后不会马上写入磁盘,因为频繁的IO操作会带来微小的开销,会大大降低Redis的性能,协定文本会被写到Redis服务器中的aof_buf中去,也叫AOF的写入缓冲区。
你这全副都写到缓冲区去了,啥时候落地?
每当serverCron
(先有一个定时工作的概念,上面马上就会讲serverCron是啥)被执行的时候,flushAppendOnlyFile
这个函数就被调用。
这个命令会调用 write
将写入缓冲区的数据写入到AOF文件中,然而这个时候还是没有真正的落到磁盘上。这是OS为了进步写入文件的效率,会将数据临时写入到OS的内存的缓冲区内,等到缓冲区被填满了或超过了指定的工夫,才会调用fsync
或者sdatasync
真正的将缓冲区的内容写入到磁盘中。
然而如果在这期间机器宕了,那么数据依然会失落。所以如果想要真正的将AOF文件保留在磁盘上,必须要调用下面提到的两个函数才行。
ServerCron
作用
当初咱们就来具体聊一下serverCron函数,它次要是用于解决Redis中的惯例工作。
什么叫惯例工作?
就比方下面提到的AOF写入缓冲区,每次serverCron执行的时候就会把缓冲区内的AOF写入文件(当然,OS会写入本人的buffer中)。其余的就像AOF和RDB的长久化操作,主从同步和集群的相干操作,清理生效的客户端、过期键等等。
那这个cron距离多久执行一次?
很多博客是间接给出的论断,100ms
执行一次,口说无凭,咱们间接撸源码。上面是serverCron的函数定义。
/* This is our timer interrupt, called server.hz times per second. * ............. */int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... server.hz = server.config_hz;}
为了防止影响大家的思路,我省略了临时对咱们没用的代码和正文。能够看到正文中有called server.hz times per second
。意思就是serverCron这个函数将会在每一秒中调用server.hz次,那这个server.hz又是啥?
server.hz
置信大家都晓得HZ(赫兹)这个单位,它是频率的国内单位制单位,示意每一条周期性事件产生的次数。所以,咱们晓得这个配置项是用于管制周期性事件产生的频率的。
其赋值的中央在下面的函数中曾经给出,能够看到其初始值是来源于redis.conf
的配置文件。那让咱们看一下具体的配置。
# Redis calls an internal function to perform many background tasks, like# closing connections of clients in timeout, purging expired keys that are# never requested, and so forth.## Not all tasks are performed with the same frequency, but Redis checks for# tasks to perform according to the specified "hz" value.## By default "hz" is set to 10. Raising the value will use more CPU when# Redis is idle, but at the same time will make Redis more responsive when# there are many keys expiring at the same time, and timeouts may be# handled with more precision.## The range is between 1 and 500, however a value over 100 is usually not# a good idea. Most users should use the default of 10 and raise this up to# 100 only in environments where very low latency is required.hz 10
简略的提取一下有用的信息,Redis会在外部调用函数来执行很多后盾的工作,而调用这些函数的频率就由这个hz
来决定的,其默认值为10
。那也就是说,下面提到的 serverCron函数会在一秒钟执行10次,这样均匀下来就是每100ms(1000ms/10)调用一次。
写入策略
下面说到,如果Redis的AOF曾经位于OS的缓冲中,如果此时宕机,那么AOF的数据同样会失落。
你这不行啊,那你这个长久化有什么意义?怎么样数据能力不失落?
这得聊一下AOF日志的写入策略,它有三种策略,别离如下:
- always 每个命令都会写入文件并且同步到磁盘
- everysec 每秒钟同步一次数据到磁盘
- no 不强制写,期待OS本人去决定什么时候写
很显著always
这种策略在真正的生产环境上是不可取的,每个命令都去写文件,会造成极大的IO开销,会占用Redis服务器的很多资源,升高Redis的服务效率。
而如果应用everysec
策略的话,即便产生了断电,机器宕机了,我最多也只会失落一秒钟的数据。
而no
则齐全交与操作系统去调度,可能会失落较多的数据。
????????,那这AOF文件咋用的,怎么复原?
下面提到过,AOF文件是记录了来自客户端的所有写命令,所以服务器只须要读入并重放一遍即可将Redis的状态复原。
然而,Redis的命令只能在客户端中的上下文才可能执行,所以Redis搞了一个没有网络连接的伪客户端来执行命令,直到命令执行结束。
老铁,你这不行啊,万一AOF日志数据量很大,你这岂不是要复原很长时间,那服务岂不是不可用了?
确实,随着服务器的运行,AOF的数据量会越来越大,重放所须要的工夫也会越来越多。所以Redis有一个重写(AOF Rewrite)机制,来实现对AOF文件的瘦身。
尽管名字叫对AOF文件的瘦身,然而实际上要做的操作跟之前曾经生成的AOF文件没有一毛钱的关系。
所谓瘦身是通过读取Redis服务器以后的数据状态来实现的,当然,这里的以后是在服务器失常运行的时候。其实你也能够了解为快照,只不过不是实打实的二进制文件了,而是间接设置快照值的命令。
用人话举个例子,假如你Redis中有个键叫test
,它的值的变动历史是1 -> 3 -> 5 -> 7 -> 9这样,那么如果是失常的AOF文件就会记录5条Redis指令。而AOF Rewrite此时染指,就只会记录一条test=9
这样的数据。
而之前的AOF文件还是照常的写入,当新的AOF文件生成后替换即可。
你tm在逗我?你在rewrite的同时,服务器依然在解决失常的申请,此时如果对服务器的状态做了更改,你这个瘦身之后的AOF文件数据不就不统一了?
这种状况确实会呈现,然而Redis通过一个AOF重写缓冲区来解决了这个问题。
当rewrite开始后,Redis会fork一个子过程,让子过程来实现AOF的瘦身操作,父过程则能够失常解决申请。AOF重写缓冲区会在rewrite开始创立了子过程之后开始应用,此时Redis服务器会把写的指令同时发送到两个中央:
- aof_buf,也就是下面提到的AOF文件的写入缓冲区
- AOF重写缓冲区
你可能会问,为啥要记录到两个中央?下面提到过,Redis执行瘦身操作时,惯例的AOF文件依然是失常生成的,所以新的Redis指令肯定会发送到写入缓冲区。
而发送到AOF重写缓冲区是为了重放在瘦身操作进行当中对Redis状态进行的更改,这样瘦身之后的AOF文件状态能力保障与Redis的状态统一。总的来说,就是为了保障瘦身的AOF文件中的数据状态与Redis过后的内存状态保持数据上的一致性。
End
对于Redis数据长久化的问题,就先聊这么多,下一期的打算的应该就是聊一聊Redis的高可用的相干机制了,感兴趣的能够微信搜寻「SH的全栈笔记」继续关注,公众号会比其余的平台先推送。
来日文章:
- Redis根底-分析根底数据结构及其用法
- 浅谈JVM和垃圾回收
- 简略理解一下K8S,并搭建本人的集群
- WebAssembly齐全入门-理解wasm的前世今生
- 简略理解InnoDB底层原理