大家好,我是Redis,一个叫Antirez的人把我带到了这个世界...图片来自Pexels,我是Redis,我的兄弟MySQL被我干掉了。说起我的出生和关系数据库MySQL还是挺有关系的。在我来到这个世界之前,MySQL的日子不好过,互联网发展越来越快,它容纳的数据越来越多,用户请求也暴增,每一个用户请求都变成了对它的一次读写操作,而MySQL很惨。尤其是“双十一”和“618”这几天全民疯狂购物的日子,MySQL更是苦不堪言。据MySQL后来告诉我的,其实超过一半的用户请求都是读操作,他们经常重复查询一件事情,浪费了大量的磁盘I/O时间。后来有人疑惑,是不是可以向CPU学习,给数据库加个缓存呢?于是我诞生了!出生后不久,我就和MySQL成为了好朋友,我们两个人经常携手出现在后端服务器。应用程序向MySQL查询的数据要在我这里注册,以后要用的时候先问我,我不会再问MySQL了。为了方便使用,我支持几种数据结构的存储:StringHashListSetSortedSetBitmap······因为我把注册的数据全部记录在内存中,没有执行慢速的I/O操作,所以节省了很多找我的时间比找MySQL还多。别小看这个简单的改动,我可以减轻MySQL的负担!随着程序的运行,我缓存的数据越来越多,相当一部分时间我都阻塞了用户的请求,这回真是太好玩了!随着我的加入,Web服务的性能有了很大的提高,这要归功于我对数据库进行了大量的测试。CacheExpiration&CacheRetirement但是很快我发现不对劲。我缓存的数据都在内存中,但是即使在服务器上,内存空间资源还是很有限的,不能这样毫无节制的保存。我得想办法,不然我就吃枣丸了。很快,我想到了一个办法:给缓存的内容设置一个超时时间,具体的设置交给应用程序。我所要做的就是删除我的过期内容并及时腾出空间。.有超时,我应该什么时候做这个清理?最简单的方法是定期删除。我决定每100ms做一次,也就是每秒10次!清理的时候无法清除所有过期的我这里存了很多数据,如果要全部扫描一遍不知道要多久,会严重影响接收新的客户要求!时间紧,任务重,只好随机抽取一些。清理可以缓解内存压力。就这样过了一段时间,发现有些key值是幸运的,每次都没有被我的随机算法选中,每次都活下来了。内存空间小!震撼而冰冷!眼睛里的沙子我擦都擦不掉!所以在原来定时删除的基础上,我又加了一个技巧:那些逃过我的随机选择算法的键值,一旦遇到查询请求,删除我发现它已经过期了,所以我会毫不犹豫地立即删除它.因为这个方法是被动触发的,不查询就不会发生,所以也叫懒删除!但是还是有一些键值逃过了我的随机选择算法,没有被查询到,导致一直逍遥法外!与此同时,可用的内存空间越来越少。而且我就算退一步说,我可以删除所有过期的数据,那么如果过期时间设置的时间长了,还没来得及清理,内存就满了,我还是得吃枣丸,所以我得想个办法。苦思冥想半天,终于想出了一个大招:内存淘汰策略,这次我要彻底解决问题!我提供了8种策略供应用选择,用于遇到内存不足时如何决策:noeviction:返回错误,不会删除任何key-values。allkeys-lru:使用LRU算法删除最近最少使用的键值。volatile-lru:使用LRU算法从设置过期时间的键集中删除最近最少使用的键值。allkeys-random:从所有键中随机删除。volatile-random:从设置了过期时间的键集中随机删除。volatile-ttl:从设置过期时间的keys中删除剩余时间最短的key。volatile-lfu:从配置过期时间的keys中移除最不常用的keys。allkeys-lfu:从所有键中删除最不常用的键。有了上面的组合拳,我再也不用担心过期数据太多填满空间的问题了~缓存穿透&Bloomfilter我的日子过得还算惬意,但是MySQL就没有我舒服了是的,有时候遇到一些烦人的请求,查询的数据不存在,MySQL会白忙活!不仅如此,因为不存在,我没法缓存,所以每次都是同样的请求。让MySQL无所不能。我作为缓存的价值还没有体现出来!这就是人们常说的缓存穿透。一来二去,MySQL大哥就忍不住了:“哎,大哥,能不能帮我想办法屏蔽掉那些我知道不会有结果的查询请求。”这时,我想到了我的另一个好朋友:Bloomfilter。我朋友没有别的本事,但是他擅长从一个非常大的数据集中快速告诉你你要找的数据是否存在(悄悄告诉你,我朋友有点不靠谱,如果它告诉你它存在,你不能全信,事实上它可能不存在,但如果它告诉你它不存在,那它一定不存在)。我把应用程序介绍给了这位朋友,这样我就不用为不存在的数据去烦MySQL了,轻松的帮助解决了缓存穿透的问题。Cachebreakdown&cacheavalanche之后,有过一段时间的平静生活,直到那天...有一次,MySQL小哥悠闲的钓鱼,突然一大堆请求找上门来,他叫我措手不及。忙了一会,MySQL就气呼呼地跑来找我,“哥,怎么回事,怎么来的这么突然?”我查看了日志,连忙解释道:“大哥,真的很抱歉,刚才一个热点数据过期了,我把它删掉了。不幸的是,这个数据有大量的查询请求,已经被删除了,所以所有请求已发送给您。”“你干嘛呢,下次注意点”,MySQL小哥一脸不爽的离开了。这件小事本来也没怎么放在心上,后来也就抛在脑后了,没想到几天后,我又捅了一个更大的筐。那天再次向MySQL发送了大量的网络请求,比之前的大了很多,MySQL小哥不一会儿就倒下了好几次!这一波流量过了很久才过去。MySQL松了一口气。“哥,这次是什么原因?”,MySQL哥累得没力气了。“这次比上次倒霉,这一次大量数据几乎同时过期,然后又出现了很多对这些数据的请求,所以规模比上次大。”MySQL哥一听,眉头一皱,“那你想想办法,折磨我三天两次,谁受得了?”和应用对话,让他均匀设置缓存过期时间?至少不要让大量数据集体失效。”“走,我们一起走。”后来我们两个去app里商量,不仅随机了key值的过期时间,还设置了热点数据永不过期,问题缓解了很多。哦,对了,我们还把这两个问题分别命名为缓存击穿和缓存雪崩。我们终于又过上了小康生活了……彩蛋:那天,我正在努力工作,不小心犯了一个错误,整个过程崩溃了。再次启动的时候,之前缓存的数据全部没有了,暴风雨般的请求又全部跑到小弟MySQL那里去了。唉,要是能记住崩溃前缓存的内容就好了……突然挂了!Redis缓存在内存中,到此结束!“起床!起床!”,含糊不清,直到有人叫我。我缓缓睁开眼睛,原来身边站着一个MySQL大哥。“我怎么睡着了?”“哎,你刚才是不是弄错了,整个过程崩溃了!好多查询请求都发给我了!”,MySQL说。刚睡醒,脑子还有些迷糊,MySQL大哥扶我起来继续干活。“糟糕!我之前缓存的数据全没了!”“卧槽?你没做坚持吗?”MySQL小哥一听脸色变了。我不好意思地摇头,“我都存到内存里了,所以才这么快。”“那你也可以保存在硬盘上,这样的话,你就从头开始建立缓存,不浪费时间!”我点点头,“让我想一想,看看这个坚持要怎么做。”在RDB持久化的几天内,我想出了一个解决方案:RDB。由于我的数据存储在内存中,最简单的方法是遍历它并将它们全部写入一个文件。为了节省空间,我定义了一个二进制格式,把数据一个一个放在一起,生成一个RDB文件。不过我的数据量有点大,一次性全部备份起来会很费时间,所以不能太频繁,不然就不用做生意了,花时间就好了在备份上。还有,如果一直没有写操作,全是读操作,那我也不需要重复备份,浪费时间。想了想,还是决定提供一个配置参数,既可以支持定期备份,又可以避免做无用功。像这样:save9001#900秒(15分钟)写入1次save30010#300秒(5分钟)写入10次save6010000#60秒(1分钟)写入10000次多个条件可以组合使用,只要以上其中之一即可条件满足,我会做备份。后来又想了想,还是不行,得fork一个子进程来做,不能浪费时间。有了备份文件,下次遇到死机退出,甚至服务器断电罢工,只要我的备份文件还在,启动时就可以读取,快速恢复到之前的状态!MySQL:binlogI有了这一套图,我兴冲冲地拿给MySQL弟弟看,希望他能给我一些鼓励。“大哥,你的计划有点问题。”没想到,他给我泼了一盆冷水。“问题?什么问题?”“你看,你定期备份,而且周期还是分钟级的,你知道我们的服务每秒响应多少请求,有多少数据不能丢失吗?”,MySQL认真地说。我有些喘不过气来,“不过这个备份需要一次遍历所有数据,开销还是挺大的,不适合高频执行。”“谁让你一次遍历所有数据的?来,我给你看个东西”,MySQL小哥带我来到一个文件目录:mysql-bin.000001mysql-bin.000002mysql-bin.000003···”看,这些是我的二进制日志binlog,猜猜里面有什么?”,MySQL小哥指着这一堆文件说道。我看了一眼,全是一堆二进制数据。我听不懂,摇了摇头。“它记录了我对数据进行的所有操作,如INSERT、UPDATE、DELETE等,当我要恢复数据时,这些操作就派上用场了。”听了他的话,我的灵感来了!告别我的兄弟MySQL,我回去研究新的解决方案。AOF持久化你也知道,我也是基于命令式的,日常工作就是响应业务程序发来的命令请求。回来后,我决定效仿MySQL大哥,把我执行的所有写命令都记录下来,写到一个文件中,并给这种持久化方式起了个名字:AOF(AppendOnlyFile)。但是,我在使用RDB解决方案时遇到了同样的问题。我应该多久写一次文件?我绝对不能把每一个写入文件的命令都记录下来,这样会严重拖累我的性能!我决定先准备一个缓冲区,然后先把要记录的命令暂时保存在这里,然后再择机写入文件。我称这个临时缓冲区为aof_buf。照做吧,我试了一下,发现数据没有写入文件。多方查询才知道,操作系统也是有缓存区的。我写的数据是他缓存的,并没有给我写入文件。这不是骗人的吗!写完好像得刷新一下,记下数据。想了想还是提供一个参数让业务程序设置什么时候刷新。appendfsync参数,三个取值:always:每个事件周期同步刷新。everysec:每秒同步刷新。不:我只是写,让操作系统决定什么时候真正写。AOFrewrite这次我没有以前那么冲动了。决定先测试一段时间再告诉MySQL小弟,免得再被他戳戳。试用了一段时间后,一切正常,但是我发现随着时间的推移,我写的AOF备份文件越来越大了!不仅占用大量硬盘空间,复制、移动、载入分析都非常麻烦,也很耗时。我必须找到一种方法来压缩文件。我把这个过程称为AOF重写。一开始打算分析原AOF文件,然后去掉里面多余的指令来瘦身AOF文件,但是很快就放弃了这个想法,工作量太大,而且分析起来相当困难麻烦,浪费精力和时间。原来一个一个记录的方式真的很蠢。数据变来变去,有很多中间状态是没用的。为什么我不直接记录最终的数据状态呢?例如:RPUSHname_list'A'RPUSHname_list'B'RPUSHname_list'C'可以合并为一个:RPUSHname_list'A''B''C'。有了重写AOF文件的想法,但是还是要花很多时间去做。决定以与RDB相同的方式fork一个子进程来执行此操作。像我一样谨慎。这样做之后,如果我在重写子进程的时候修改了数据,就会和重写的内容不一致!MySQL大哥肯定会批评的,这个漏洞我得补上。所以,除了之前的aof_buf,我还准备了另一个buffer:AOFrewritebuffer。从创建重写子进程的那一刻起,我还将以下写入命令复制到这个重写缓冲区中。子进程重写AOF文件后,我将把这个缓冲区中的命令写入新的AOF文件。最后,重命名新的AOF文件并替换原来臃肿的文件,大功告成!确定自己的思路没有问题后,我又找到了MySQL师兄一个新的方案,我都做到了这一次,我想他这次应该没什么好说的了吧?mysql小哥看到我的方案后露出了满意的笑容,随口问了一句:这个AOF方案这么好,RDB方案能不能扔掉?什么?没想到他居然问我这个问题,我陷入了沉思。你觉得我应该怎么回答?彩蛋:“怎么又崩溃了?”,“对不起,又遇到bug了,不过不用担心,我很快就能恢复过来的!”。“那个老掉牙问题不大,你只有一个实例,太不靠谱了,找帮手!”那天,我被拉进了一个Redis群聊……那天,在Redis的朋友群里,好久不见了。看到大白发了一条信息……于是大白建了一个新群:以后的日子,我们三个人互相配合,日常工作中最常见的事情就是数据同步。如果主节点有数据写入、删除、修改命令,也会将这些命令一一通知从节点。我们称之为命令传播。这样我们的主节点和从节点之间的数据就可以保持同步了!有一次,我不小心掉线了~我们使用了新的数据同步策略,效率更高了。该行还可以快速填充缺失的数据。这样过了一段时间……新的人手加入,准备大干一场!为了及时获取和更新主从节点的信息,我们的哨兵每隔十秒就会使用INFO命令向主节点问候一次。主节点会告诉我他有哪些从节点!为了及时知道大家是否下线,我们的哨兵每秒都会用PING命令向群里所有的朋友问好:如果在规定的时间内没有收到回复,我就知道这家伙可能跪了,是时候开始故障转移了。但这只是我的主观看法,我一个人不算数。为了防止误判,只好去管理员小群里征求大家的意见:接下来,我们开始第一次选举。经过一番努力,我终于完成了故障转移,R2现在是主节点。但没过多久,R1又回来了:以上就是我们的日常工作。经过几位小伙伴的共同努力,一个高可用的缓存服务已经形成。MySQL大哥不敢再小看我们了。作者:轩辕志峰编辑:陶佳龙来源:转载自公众号编程科技宇宙(ID:xuanyuancoding)
