内容来自于小破站UP主,编程技术宇宙的趣话Redis系列视频
一、我是谁?
我是redis,是由Antirez带我来到这个世界上的。
我的诞生和MySQL非常有缘分,在还没有我的时候,MySQL非常辛苦,由于互联网快速的发展,数据量也越来越多,用户请求也随之暴涨,而每一个用户请求,都对他进行一个又一个的读写操作,MySQL苦不堪言。尤其是到了双十一,618这种全民购物的日子,都是MySQL受苦受难的日子。
后来MySQL告诉我,其实用户请求大多数都是读操作,而且经常重复查询同一个数据,浪费他很多的时间去进行磁盘I/O。有人就开始琢磨,是否可以像CPU一样给数据库也加一个缓冲呢?
于是,我就诞生了!!!
二、Redis缓存管理机制
在我出生不久,我就和MySQL成为好朋友,经常一起出现在后端服务器中。
应用程序在MySQL中查询到的数据在我这里进行登记,后面需要用到的时候0先找我要,如果我没有的话,再去找MySQL。
因为我把数据都存储在内存中,所以不需要去执行慢入蜗牛的I/O操作,所以找我会比找MySQL节约更多的时间。
通过我的加入,我为MySQL减轻了不小的负担。随着程序的运行,我缓存的数据也越来越多,大部分时间,我都为MySQL挡住了用户请求。但是我意识到了事情的严重性,因为我存储在内存中,不能无节制的存下去,不然我迟早要完。
不久我想到了一个办法,就是给缓存的内容设置一个超时时间,具体设置多久留给应用程序自己决定,我只负责把过期的内容及时删除,腾出空间即可。我决定100毫秒去做一次这样的操作。1秒就是10次,但因为我里面存了大量的数据,如果都检测一遍的话就太占用时间了,会严重影响接待新的用户请求。时间紧,任务中。我只好随机选择一部分,能缓解内存压力即可。
这样运行了一段时间,我发现有些数据的运气比较好,每次都没有被我的随机算法选中。这可不行。于是在原来定期删除的基础上,我加了一招,一旦遇到查询请求,如果发现他已经过期,就立即删除,这种方式因为是被动式触发的,不查询就不会发生,因此也叫惰性删除。
但是,还是有部分的数据,既逃脱了定期删除又躲过了惰性删除。导致他们一直逍遥法外,可以使用的内存也越来越少,而且就算退一步讲,我可以把过期的数据都删除掉,如果数据过期时间太长,还没等到我清理,内存就已经满了,那该怎么办?
我苦思良久,终于想出一个办法,那就是内存淘汰策略。这次我要彻底解决问题!!!
内存淘汰策略:
- no-envicition:
该策略对于写请求不再提供服务,会直接返回错误,当然排除del等特殊操作,redis默认是no-envicition策略。(返回错误,不会删除任何键值)- allkeys-random:
从redis中随机选取key进行淘汰- allkeys-lru:
使用LRU(Least Recently Used,最近最少使用)算法,从redis中选取使用最少的key进行淘汰- allkeys-lfu:
使用LFU(Least Frequently Used,最不经常使用),从所有的键中选择某段时间之内使用频次最少的键值对清除- volatile-random:
从redis中设置过过期时间的key,进行随机淘汰- volatile-ttl:
从redis中选取即将过期的key,进行淘汰- volatile-lru:
使用LRU(Least Recently Used,最近最少使用)算法,从redis中设置过过期时间的key中,选取最少使用的进行淘汰- volatile-lfu:
使用LFU(Least Frequently Used,最不经常使用),从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉
我提供了八种策略供应用程序选择,用于我内存不足时该如何决策。
有了上面的几个组合拳,我的日子总算过的比较舒坦了。
但是MySQL就没这么舒坦了!
有时候遇到烦人的请求,查询的数据不存在,MySQL就会白忙活一场,但是因为数据不存在,我也没办法缓存,因此作为缓存,我的价值就没有完全体现。这就是缓存穿透
有一次,MySQL悠哉游哉的摸鱼,突然一大堆请求给了MySQL。于是MySQL问我怎么回事。我解释到,刚刚一个热点数据到了过期时间,被我删除了,不巧的是,随后就有对这个数据的大量查询请求,因为我已经 删除了,所以都到了你那边了。这也是缓冲击穿MySQL一脸不高兴的走了。我也没放在心上。
不久,又有大量的请求,发送到了MySQL那边,比上次还多,不一会MySQL就被干趴下了好几次,等了好半天,这波流量才算过去。这就是缓存雪崩。
MySQL又问我这次为什么?我回答到,这次更不巧,好多数据同时到了过期时间,但紧接着又对这些数据发来了一大批请求,所以比起上次的规模就更大了。
但是过期时间不是我定的,因此我和MySQL找应用程序进行了商量,不仅把过期时间均匀分布,还设置了热点数据永不过期。这个问题就缓解了不少。我们又过上了舒坦的日子。
有一次,我专心的工作中,不小心出了错。整个进程都崩溃了。当我再次启动后,之前缓存的数据全没了,于是大量的请求又到了MySQL那边。如果我可以记住崩溃前缓存的数据就好了…
三、Redis持久化存储机制
通过与MySQL的交流,它推荐我把数据进行持久化到硬盘。于是我拿出了一套方案:RDB持久化。
RDB:我把所有的数据遍历一遍,全部以二进制的格式写入扩展名为rdb的文件中。但是我的数据有点多,全部备份一遍,太浪费时间了,所以不能太频繁的做这件事,还有如果一直是读取数据,没有写入数据,我也没必要一直备份。
于是,我决定提供一套配置参数,既可以进行周期性备份,又可以避免做无用功。
save 900 1 # 900秒内有1个写入
save 300 10 # 300秒内有10个写入
save 60 10000 # 60秒内有10000个写入
后来,我又想了一下,不能自己做这件事情,需要创建一个子进程去做这件事情。这样就不会丢失自己的数据了。
于是我找MySQL说了一下这个方案。他觉得有点问题。
每分钟就会产生大量请求,你几分钟才备份一次,那崩溃时,岂不是会损失很多数据吗?于是MySQL让我看了它的二进制日志文件binlog。这里面记录的是我对数据执行的所有动作,如果进行数据恢复就会排上很大用途。
因为我也是基于命令的,于是我回去想出了新的方案:AOF持久化,像MySQL一样,把所有的操作记录存储到一个文件。同样AOF方案中遇到了与RDB相同的问题,我该多久写入一次文件呢?
于是我决定搞一个缓冲区,把命令先写入缓存中,然后在择机写入文件。我把这个缓存区叫aof_buf。
这次我先试用了一段时间,但是一段时间过去,我发现这个备份文件越来越大。不仅耗费空间,加载移动也非常耗费时间。于是我想了一个办法,把文件压缩一下,这个办法我把它叫AOF重写。原来的一条条记录太笨了。数据改来改去,有很多中间状态都没用到,因此我只要把最终状态记录下来就可。
思路有了但是它还是很耗时间,于是我决定把它和RDB一样,用一个子进程去执行这个事情。但在重写的过程中,如果我修改了数据,就会出现与重写的数据不一致。
于是我在原来的aof_buf的基础上,有准备了一个缓冲区aof重写缓冲区。从创建重写子进程开始,后面来的写入命令也copy到这个重写缓冲区中,等到子进程重写文件之后。我再把这个缓冲区中的命令写入新的aof文件中。最后在重命名新的文件,替换原来臃肿的旧文件。
这下就完成了数据的持久化。
有一天,我又崩溃了,虽然我可以很快的恢复数据,但是MySQL却说,一直崩溃也不是办法,你只有一个实例太不可靠,去找几个帮手吧…
四、Redis哨兵与高可用原理
那天,Redis群中,大白发来了一条消息。大家聊到了被MySQLdiss,无法高可用。于是大白想了一个办法。
咱们组一个Team,用主-从模式,主节点负责写数据,从节点负责读数据,然后做好数据同步,读写分离提高性能,另外主节点崩溃了,从节点就顶上去,还能实现高可用。
听了这个办法,我。小黑,大白组成了主从同步群。大白先做主节点,因为主节点任务重,需要负责数据写入和数据同步。让我和小黑先做从节点,熟悉熟悉。
以后我们的工作最多的就是同步数据。如果主节点有数码写入、删除、修改命令,也会把这些命令一个个通知给从节点。我们把它叫命令传播
通过这样的方式,我们主节点与从节点之间数据就能保持同步了~
有一次我的网络不小心掉线了。于是大白让我把最新的RDB文件同步一下。但是我只是网络掉线一小会,没必要把所有的数据全部同步,太浪费时间了。
于是小黑提出了一个办法
主节点内部准备一个缓冲区,传播命令的时候,除了同步给从节点,也往这个缓存区中写入一份,这样把最近的命令保存起来,下次再掉线的时候,就把最新的同步就好了。
其中遇到一个问题,就是主节点不知道缓冲和从节点确实能不能对应上,也不知道需要提供缓存中的哪些。
于是小黑想出了弄一个游标,叫做复制偏移量。最开始都是0,随着数据复制和同步大家一起更新。后面只需要比较各自的偏移量,就知道哪些是缺失数据了。
这个办法不错,但是又遇到一个问题,如果有别的节点加入我们,他里面保存了偏移量,但是和我们的不一样,我把数据给他岂不是张冠李戴了吗?
于是我想出了一个办法,运行ID。咱们每次运行时,都生成一个运行ID,随机的,每次都不一样。第一次同步时,主节点把自己的运行ID告诉从节点,后面节点断线重连都要告诉主节点他之前保存的运行ID。主节点拿到之后和自己比较一下就知道是否是从自己这里同步的数据。如果是的话就只同步缺失部分,如果不是的话就说明是新来的,就让他把全部数据同步一遍。
我们用上了新的同步策略,效率上高了不少。偶尔掉线,也可以把数据快速补上。就这样运行了一段时间
一段时间后,我提出,虽然我们有主从复制,但是主节点挂了,还是需要程序员们手动选择从节点升级为主节点来提供服务,感觉这个不够智能。
于是大白想出了一个办法,选出一个人做管理员,不用负责数据的读写,专门统筹协调,谁要掉线,就在从节点中选出来一个顶上去。我们把管理员叫哨兵
但是一个管理员很明显不够,如果一个管理员挂了,那岂不是完了。所以我们在群中找了3个小弟,让他们负责数据读写,我们三个负责管理。
为了及时获更新主从节点的信息,哨兵每隔十秒钟就要用INFO命令去问候一下主节点,主节点会告诉我他有哪些从节点
为了更加及时知道大家是否掉线,哨兵每隔一秒都要用PING命令问候一下各个节点
如果在设置的时间里没有收到回复,就知道这家伙多半是跪了,就该启动故障转移了
但为了防止误判,我们管理员之间也要进行通讯,一起决定节点是否掉线
于是小黑提出一个建议,每个管理员发现主节点掉线之后,这时候判断为主观下线,然后通知其他管理员,需要其他管理员判断是否为下线,才能认定为客观下线,再启动后面的故障转移操作。
故障转移
第一步选个新的主节点,第二步,让其他从节点从新的主节点那里同步数据,第三步,把原来旧的主节点改成从节点
但不能确定要让谁去做主节点。
于是大白想出了一个办法,可以给不同节点设定优先级,硬件配置高的优先级越高,挑选的时候参考这个优先级来
我也想出了一个办法,可以优先选择跟主节点断开连接最短的节点,这样他的数据会更新一点
小黑也提出,还可以参考复制偏移量,越大的数据应该越全
最终决定,选择优先级更高,复制偏移量最大的节点
就这样我们完成了故障转移
于是我们一同构成了高可用的缓存服务
我把这个消息告诉了MySQL,他说你们每个节点保存了海量数据,数据量太多…
五、Redis集群是如何工作的
随着时间的推移,我们的数据量越来越大。虽然有主从复制加哨兵但只能解决高可用问题。无法解决数据量问题。虽然人多,但是都保存这相同的数据,无法解决大容量问题。
于是我找大白和小黑,计划把我们的所有内存合在一起成为一个大的缓存服务器,变成一个集群。
既然是集群就要考虑管理问题
如何扩容,有新的成员加入。我们抄袭TCP的三次握手,握手协议。想要加入集群,就需要有一个推荐人,通过团队中的任何一个人都可以。比如说我,只要告诉我IP和端口,我就会给他发送一个MEET信息发起握手,对方需要回一个PONG信息,同意入伙,最后我在发送一个PING信息,三次握手就完成了。最后,我再把这个成员告诉团队中的其他成员。
解决数据存储的公平问题,学习哈希表的方法。
我们总共划分了16384个哈希桶,我们把它叫槽位Slot,程序员可以按照各自能力大小,给我们分配槽位。读写的时候,对键值做一下哈希计算,映射到那个槽,就让那个负责。
为了让大家的信息达成一致,启动的时候需要把自己的槽位告诉其他成员。一万多个槽位数据量挺大的。为了压缩数据空间,于是我们把每个槽位用bit代替,自己负责的位是1否则就是0.总共16384个bit,也就是2048个字节,传送起来很快。
虽然传送方便了,但是工作的时候却不方便,遇到读写数据时,总不能挨个看谁的那一位是1。
于是我们用空间换时间,我们准备了一个超大的数组来存储,来存储每个槽由哪个节点负责,通过上面的方式拿到信息后更新到这里,这样遇到数据访问的时候我们就能快速的知道这个数据是由谁负责。
这16384个槽位必须都要有人来负责,集群状态才算上线,否则就是下线状态。
数据分派的问题解决了。团队总算可以正式上线工作了。
只是多了一个步骤,如果存储数据时,看看是否时自己负责,如果不是就返回一个MOVEO错误携带负责这个数据的槽号,IP和端口,让他知道找谁处理。
但是程序员是感知不到的,因为他们用封装好的库进行操作。
经过一段时间,我们配合很默契,但是还是不行,如果那个成员挂了,集群就下线了。所以没人必须有一个backup才可以,所以让原来的小弟继续给我们做从节点,平时当我们backup从我们这里复制数据,一旦我们遇到故障他们就能快速顶上。
有了集群工作+主从复制。我们现在不仅高可用,数据容量也大大提升,就算不够用了也可以扩容。
官方推荐集群用哨兵模式