周末,我和几个朋友闲聊,其中有一个朋友说他被华为一面的面试官吊打了!
我这个朋友是做Java的,有2年的工作经验,本来去面华为是非常自信的,但一面的时候,面试官问了他这个问题“如何保证缓存与数据库的双写一致性?”直接把他击败了!
我们先来剖析一下这个题,一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上请求。
其实很多人都会像我这个朋友一样,吃面试的亏。但是怎么才能避免呢,最好的办法就是真正的掌握底层原理的相关知识。为了让更多人避免踩面试的坑,今天我将分享——7大缓存经典问题
实际上,在缓存系统的设计架构中,还有很多坑,很多的明枪暗箭,设计不当会导致很多严重的后果。如果设计不当,轻则请求变慢、性能降低,重则会数据不一致、系统可用性降低,甚至会导致缓存雪崩,整个系统无法对外提供服务。本课时,我将对缓存设计中的7大经典问题,进行问题描述、原因分析,并给出日常研发中,可能会出现该问题的业务场景,最后给出这些经典问题的解决方案。
缓存第一个经典问题是缓存失效。上一课时讲到,服务系统查数据,首先会查缓存,如果缓存数据不存在,就进一步查DB,最后查到数据后回种到缓存并返回。缓存的性能比DB高50~100倍以上,所以我们希望数据查询尽可能命中缓存,这样系统负荷最小,性能最佳。缓存里的数据存储基本上都是以key为索引进行存储和获取的。业务访问时,如果大量的key同时过期,很多缓存数据访问都会miss,进而穿透到DB,DB的压力就会明显上升,由于DB的性能较差,只有缓存的1%-2%以下,这样请求的慢查率会明显上升。这就是缓存失效的问题。
导致缓存失效,特别是很多key一起失效,跟我们日常写缓存的过期时间息息相关。在写缓存时,我们一般会根据业务的访问特点,给每种业务数据预制一个过期时间,在写缓存时把这个过期时间带上,让缓存数据在这个固定的过期时间后被淘汰。一般情况下,因为缓存数据是逐步写入的,所以也是逐步过期被淘汰的。但在某些场景,一大批数据会被系统主动或被动从DB批量加载,然后写入缓存。这些数据写入缓存时,由于使用相同的过期时间,在经历这个过期时间之后,这批数据就会一起到期,从而被缓存淘汰。此时,对这批数据的所有请求,都会出现缓存失效,从而都穿透到DB,DB由于查询量太大,就很容易压力大增,请求变慢。
很多业务场景,稍不注意,就出现大量的缓存失效,进而导致系统DB压力大、请求变慢的情况。比如同一批火车票、飞机票,当可以售卖时,系统会一次性加载到缓存,如果缓存写入时,过期时间按照预先设置的过期值,那过期时间到期后,系统就会因缓存失效出现变慢的问题。类似的业务场景还有很多,比如微博业务,会有后台离线系统,持续计算热门微博,每当计算结束,会将这批热门微博批量写入对应的缓存。还比如,很多业务,在部署新IDC或新业务上线时,会进行缓存预热,也会一次性加载大批热数据。
对于批量key缓存失效的问题,原因既然是预置的固定过期时间,那解决方案也从这里入手。设计缓存的过期时间时,使用公式:过期时间=baes时间+随机时间。即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对DB造成过大压力。
第2个经典问题是缓存穿透,缓存穿透是一个很有意思的问题。因为缓存穿透发生的概率很低,所以一般很难被发现。但是,一旦你发现了,而且量还不小,你可能立即就会经历一个忙碌的夜晚。因为对于正常访问,访问的数据即便不在缓存,也可以通过DB加载回种到缓存。而缓存穿透,则意味着有特殊访客在查询一个不存在的key,导致每次查询都会穿透到DB,如果这个特殊访客再控制一批肉鸡机器,持续访问你系统里不存在的Key,就会对DB产生很大的压力,从而影响正常服务。
缓存穿透存在的原因,就是因为我们在系统设计时,更多考虑的是正常访问路径,对特殊访问路径、异常访问路径考虑相对欠缺。缓存访问设计的正常路径,是先访问cache,cache miss后查DB,DB查询到结果后,回种缓存返回。这对于正常的key访问是没有问题的,但是如果用户访问的是一个不存在的key,查DB返回空(即一个NULL),那就不会把这个空写回cache。那以后不管查询多少次这个不存在的key,都会cache miss,都会查询DB。整个系统就会退化成一个前端+DB的系统,由于DB的吞吐只有cache的1%~2%以下,如果有特殊访客,大量访问这些不存在的key,就会导致系统的性能严重退化,影响正常用户的访问。
缓存穿透的业务场景很多,比如通过不存在的UID访问用户,通过不存在的车次ID查看购票信息。用户输入错误,偶尔几个这种请求问题不大,但如果是大量这种请求,就会对系统影响非常大。那么如何解决这种问题呢,其实也有应对之策。第一种方案就是,查询这些不存在的数据时,第一次查DB,虽然没查到结果返回NULL,仍然记录这个key到缓存,只是这个key对应的value是一个特殊设置的值。第二种方案是,构建一个BloomFilter缓存过滤器,记录全量数据,这样访问数据时,可以直接通过BloomFilter判断这个key是否存在,如果不存在直接返回即可,根本无需查缓存和DB。
不过这两种方案在设计时仍然有一些要注意的坑。对于方案1,如果特殊访客持续访问大量的不存在的key,这些key即便只存一个简单的默认值,也会占用大量的缓存空间,导致正常key的命中率下降。所以进一步的改进措施是,对这些不存在的key只存较短的时间,让它们尽快过期;或者将这些不存在的key存在一个独立的公共缓存,从缓存查找时,先查正常的缓存组件,如果miss,则查一下公共的非法key的缓存,如果后者命中,直接返回,否则穿透DB,如果查出来是空,则回种到非法key缓存,否则回种到正常缓存。对于方案2,BloomFilter要缓存全量的key,这就要求全量的key数量不大,10亿条数据以内最佳,因为10亿条数据大概要占用1.2GB的内存。也可以用BloomFilter缓存非法key,每次发现一个key是不存在的非法key,就记录到BloomFilter中,这种记录方案,会导致BloomFilter存储的key持续高速增长,为了避免记录key太多而导致误判率增大,需要定期清零处理。
BloomFilter是一个非常有意思的数据结构,不仅仅可以挡住非法key攻击,还可以低成本、高性能的对海量数据进行与否判断,比如一个系统有数亿用户和百亿级新闻feed,就可以用BloomFilter来判断某个用户是否阅读某条新闻feed。下面我将对BloomFilter数据结构做一个分析。
BloomFilter的目的是检测一个元素是否存在于一个集合内。它的原理,是用bit数据组来表示一个集合,对一个key进行多次不同的hash检测,如果所有hash对应的bit位都是1,则表明key非常大概率存在,平均单记录占用1.2字节即可达到99%,只要有一次hash对应的bit位是0,就说明这个key肯定不存在于这个集合内。
BloomFilter的算法是,首先分配一块内存空间做bit数组,数组的bit位初始值全部设为0,加入元素时,采用k个相互独立的hash函数计算,然后将元素hash映射的K个位置全部设置为1。检测key时,仍然用这K个hash函数计算出K个位置,如果位置全部为1,则表明key存在,否则不存在。BloomFilter的优势是,全内存操作,性能很高。另外空间效率非常高,要达到1%的误判率,平均单条记录占用1.2字节即可。
而且,平均单条记录每增加0.6字节,还可让误判率继续变为之前的1/10,即平均单条记录占用1.8字节,误判率可以达到1/1000,平均单条记录占用2.4字节,误判率可以到1/10000,以此类推。这里的误判率是指,BloomFilter判断某个key存在,但它实际不存在的概率,因为它存的是key的hash值,而非key的值,所以有概率存在这样的key,它们内容不同,但多次hash后的hash值都相同。对于BloomFilter判断不存在的key ,则是100%不存在的,反证法,因为如果这个key存在,那它每次hash后对应的hash值位置肯定是1,而不会是0。
第3个问经典题是缓存雪崩,系统运行过程中,缓存雪崩是一个非常严重的问题。缓存雪崩是指部分缓存节点不可用,导致整个缓存体系甚至甚至服务系统不可用的情况。缓存雪崩按照缓存是否rehash(即是否漂移)分2种情况,即缓存不支持rehash导致的系统雪崩不可用,以及缓存支持rehash导致的缓存雪崩不可用。
这两种情况中,缓存不进行rehash时产生的雪崩,一般是由于较多缓存节点不可用,请求穿透导致DB也过载不可用,最终整个系统雪崩不可用的。而缓存支持rehash时产生的雪崩,则大多跟流量洪峰有关,流量洪峰到达,引发部分缓存节点过载crash,然后因rehash扩散到其他缓存节点,最终整个缓存体系异常。
第一种情况比较容易理解,缓存节点不支持rehash,较多缓存节点不可用时,大量Cache访问会失败,根据缓存读写模型,这些请求会进一步访问DB,而且DB可承载的访问量要远比缓存小的多,请求量过大,就很容易造成DB过载,大量慢查询,最终阻塞甚至crash,从而导致服务异常。
第二种情况是怎么回事呢?这是因为缓存分布设计时,很多同学会选择一致性hash分布方式,同时在部分节点异常时,采用rehash策略,即把把异常节点请求平均分散到其他缓存节点。在一般情况下,一致性hash分布+rehash策略可以很好的运行,但在较大的流量洪峰到临之时,如果大流量key比较集中,正好在某1-2个缓存节点,很容易将这些缓存节点的内存、网卡过载,缓存节点异常crash,然后这些异常节点下线,这些大流量key请求又被rehash到其他缓存节点,进而导致其他缓存节点也被过载crash,缓存异常持续扩散,最终导致整个缓存体系异常,无法对外提供服务。
缓存雪崩的业务场景并不少见,微博、twitter等系统在运行的最初若干年都遇到过很多次。比如,微博最初很多业务缓存采用一致性hash+rehash策略,在突发洪水流量来临时,部分缓存节点过载crash甚至宕机,然后这些异常节点的请求转到其他缓存节点,又导致其他缓存节点过载异常,最终整个缓存池过载。另外,机架断电,导致业务缓存多个节点宕机,大量请求直接打到DB,也导致DB过载而阻塞,整个系统异常。最后缓存机器复电后,DB重启,数据逐步加热后,系统才逐步恢复正常。
预防缓存雪崩,这里我给出3个解决方案。方案1,是对业务DB的访问增加读写开关,当发现DB请求变慢、阻塞,慢请求超过阀值时,就会关闭读开关,部分或所有读DB的请求进行failfast立即返回,待DB恢复后再打开读开关。方案2是对缓存增加多个副本,任何缓存异常或请求miss后,再读取其他缓存副本,而且多个缓存副本尽量部署在不同机架,从而确保在任何情况下,缓存系统都会正常对外提供服务。方案3是,对缓存体系进行实时监控,当请求访问的慢速比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。实际上,微博平台系统,这三种方案都采用了,通过三管齐下,规避缓存雪崩的发生。
第4个经典问题是数据不一致,同一份数据,可能会同时存在DB和缓存之中。那就有可能发生,DB和缓存的数据不一致。如果缓存有多个副本,多个缓存副本里的数据也可能会发生不一致现象。
不一致的问题大多跟缓存更新异常有关。比如更新DB后,写缓存失败,从而导致缓存中存的是老数据。另外,如果系统采用一致性hash分布,同时采用rehash自动漂移策略,在节点多次上下线之后,也会产生脏数据。缓存有多个副本时,更新某个副本失败,也会导致这个副本的数据是老数据。
导致数据不一致的场景也不少。在缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和DB的数据不一致。缓存rehash时,某个缓存机器反复异常,多次上下线,更新请求多次rehash。这样,一份数据存在多个节点,且每次rehash只更新某个节点,导致一些缓存节点产生脏数据。
要尽量保证数据的一致性。这里我也给出了3个方案,你可以根据实际情况进行选择方案。第一个方案,cache更新失败后,可以进行重试,如果重试失败,则将失败的key写入队列机服务,待缓存访问恢复后,将这些key从缓存删除。这些key在再次被查询时,重新从DB加载,从而保证数据的一致性。第二个方案,缓存时间适当调短,让缓存数据及早过期后,然后从DB重新加载,确保数据的最终一致性。第三个方案,不采用rehash漂移策略,而采用缓存分层策略,尽量避免脏数据产生。
第5个经典问题是数据并发竞争,互联网系统,线上流量较大,缓存访问中很容易出现数据并发竞争的现象。数据并发竞争,是指在高并发访问场景,一旦缓存访问没有找到数据,大量请求就会并发查询DB,导致DB压力大增的现象。
数据并发竞争,主要是由于多个进程/线程中,有大量并发请求获取相同的数据,而这个数据key因为正好过期、被剔除等各种原因在缓存中不存在,这些进程/线程之间没有任何协调,然后一起并发查询DB,请求那个相同的key,最终导致DB压力大增。
数据并发竞争在大流量系统也比较常见,比如车票系统,如果某个火车车次缓存信息过期,但仍然有大量用户在查询该车次信息。又比如微博系统中,如果某条微博正好被缓存淘汰,但这条微博仍然有大量的转发、评论、赞。上述情况都会造成该车次信息、该条微博存在并发竞争读取的问题。
要解决并发竞争,有2种方案。方案1是使用全局锁。即当缓存请求miss后,先尝试加全局锁,只有加全局锁成功的线程,才可以到DB去加载数据。其他进程/线程在读取缓存数据miss时,如果发现这个key有全局锁,就进行等待,待之前的线程将数据从DB回到缓存后,再从缓存获取。
方案2是,对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况。

第6个经典问题是Hot key,对于大多数互联网系统,数据是分冷热的。比如最近的新闻、新发表的微博被访问的频率最高,而比较久远的之前的新闻、微博被访问的频率就会小很多。而在突发事件发生时,大量用户同时去访问这个突发热点信息,访问这个Hot key,这个突发热点信息所在的缓存节点就很容易出现过载和卡顿现象,甚至会被crash。
Hot key引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的key,比如微博中数十万数百万的用户同时去吃一个新瓜。数十万的访问请求同一个key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU的极限,从而导致缓存访问变慢、卡顿。
引发Hot key的业务场景很多,比如明星结婚、离婚、出轨这种特殊突发事件,比如奥运、春节这些重大活动或节日,还比如秒杀、双12、618等线上促销活动,都很容易出现Hot key的情况。
要解决这种极热key的问题,首先要找出这些Hot key来。对于重要节假日、线上促销活动、集中推送这些提前已知的事情,可以提前评估出可能的热key来。而对于突发事件,无法提前评估,可以通过Spark,对应流任务进行实时分析,及时发现新发布的热点key。而对于之前已发出的事情,逐步发酵成为热key的,则可以通过Hadoop对批处理任务离线计算,找出最近历史数据中的高频热key。
找到热key后,就有很多解决办法了。首先可以将这些热key进行分散处理,比如一个热key名字叫hotkey,可以被分散为hotkey#1, hotkey#2, hotkey#3,……hotkey#n,这n个key分散存在多个缓存节点,然后client端请求时,随机访问其中某个后缀的hotkey,这样就可以把热key的请求打散,避免一个缓存节点过载。其次,也可以key的名字不变,对缓存提前进行多副本+多级结合的缓存架构设计。再次,如果热key较多,还可以通过监控体系对缓存的SLA实时监控,通过快速扩容来减少热key的冲击。最后,业务端还可以使用本地缓存,将这些热key记录在本地缓存,来减少对远程缓存的冲击。
最后一个经典问题是Big key,也就是大Key的问题。大key,是指在缓存访问时,部分Key的Value过大,读写、加载易超时的现象。
造成这些大key慢查询的原因很多。如果这些大key占总体数据的比例很小,存mc,对应的slab较少,导致很容易被频繁剔除,DB反复加载,从而导致查询较慢。如果业务中这种大key很多,而这种key被大量访问,缓存组件的网卡、带宽很容易被打满,也会导致较多的大key慢查询。另外,如果大key缓存的字段较多,每个字段的变更都会引发对这个缓存数据的变更,同时这些key也会被频繁地读取,读写相互影响,也会导致慢查现象。最后,大key一旦被缓存淘汰,DB加载可能需要花费很多时间,这也会导致大key查询慢的问题。
大key的业务场景也比较常见。比如互联网系统中需要保存用户最新1w个粉丝的业务,比如一个用户个人信息缓存,包括基本资料、关系图谱计数、发feed统计等。微博的feed内容缓存也很容易出现,一般用户微博在140字以内,但很多用户也会发表1千字甚至更长的微博内容,这些长微博也就成了大key。
对于大key,我这里给了3种解决方案。第1种方案,如果数据存在mc中,可以设计一个缓存阀值,当value的长度超过阀值,则对内容启用压缩,让kv尽量保持小的size,其次评估大key所占的比例,在mc启动之初,就立即预写足够数据的大key,让mc预先分配足够多的trunk size较大的slab。确保后面系统运行时,大key有足够的空间来进行缓存。
第二种方案,如果数据存在redis中,比如业务数据存set格式,大key对应的set结构有几千几万个元素,这种写入redis时会消耗很长的时间,导致redis卡顿。此时,可以扩展新的数据结构,同时让client在这些大key写缓存之前,进行序列化构建,然后通过restore一次性写入。
第三种方案时,将大key分拆为多个key,尽量减少大key的存在。同时由于大key一旦穿透到DB,加载耗时很大,所以可以对这些大key进行特殊照顾,比如设置较长的过期时间,比如缓存内部在淘汰key时,同等条件下,尽量不淘汰这些大key。
至此,缓存的7大经典问题就全部讲完啦。
通过本节课程,我们要认识到,对于互联网系统,由于实际业务场景复杂,数据量、访问量巨大,需要提前规避缓存使用中的各种坑。你可以通过提前熟悉Cache的经典问题,提前构建防御措施, 避免大量key同时失效,避免不存在key访问的穿透,减少大key、热key的缓存失效,对热key进行分流。你可以采取一系列措施,让访问尽量命中缓存,同时保持数据的一致性。另外,你还可以结合业务模型,提前规划Cache系统的SLA,如QPS、响应分布、平均耗时等,实施监控,以方便运维及时应对。在遇到部分节点异常,或者遇到突发流量、极端事件时,也能通过分池分层策略、key分拆等策略,避免故障发生。
最终,你能在各种复杂场景下,面对高并发、海量访问,面对突发事件和洪峰流量,面对各种网络或机器硬件故障,都能保持服务的高性能和高可用。
今天的分享到这里就结束了,如果你想加强对分布式的缓存的理解,突破自己的薪资瓶颈,你还需要更系统的学习哦~
300分钟吃透分布式缓存
免费试读课程 
点击“阅读原文”即可免费试读
或者
识别下方二维码即可
版权声明:本文版权归属拉勾教育及该专栏作者,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表,违者必究。
在看点这里
点击阅读原,我们一起进步
继续阅读
阅读原文