缓存系统是我们平时开发经常使用到的,也是在高并发场景下减少或防止流量对DB等底层系统冲击的最有效手段之一。下面就简单谈谈缓存系统经常提及的三个问题以及解决方案。
缓存穿透
首先回忆下通常情况我们设置的缓存机制,如下图所示:
这套机制,由于出于容错考虑,从存储层查不到数据则不写入缓存,这就导致每次请求不存在的数据时都要到存储层去查询。如果有黑客可以利用不存在的key,频繁请求我们的服务器,这些请求就会穿透缓存,直接打到DB上,对DB造成巨大压力甚至挂掉。这就是缓存穿透。
解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器(bloom filter)。
布隆过滤器是将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外一个更为简单粗暴的方法,如果一个查询结果空(不管是数 据不存在,还是查询异常),我们仍然把这个空结果进行缓存,但它的过期时间会很短,几分钟即可。
缓存雪崩
缓存雪崩是指在如果我们几乎在同一时间设置的缓存(比如缓存预热),并且设置了相同的过期时间,这就会导致缓存会在某一时刻同时失效,这个时间所有请求会全部转发到DB,DB瞬时压力过重雪崩。
解决方案
大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。另一个简单的方案就是我们可以在原有的失效时间基础上增加一个随机值,让缓存的失效时间错开,就可以有效的避免缓存雪崩。
缓存击穿
缓存击穿跟缓存雪崩类似,区别就是缓存雪崩是群体失效,缓存击穿是单体失效,比如一个非常热点的数据。比较典型的场景就是新浪微博的热点事件,比如鹿晗和关晓彤事件,因为超高并发的访问,如果这个时间点缓存过期,在系统从后端DB加载数据到缓存这个过程中,这段时间超大并发的请求会同时打到DB上,很有可能瞬间把DB压垮。相关推荐阅读:双11万亿流量下的分布式缓存
解决方案
1.使用互斥锁(mutex lock):
简单地来说,就是在缓存失效的时候,不是直接请求DB,而是先加分布式锁(比如redis的setNx),如果加锁成功,再进行load db的操作并回设缓存;如果加锁失败,说明已经有别的进程在加锁重设缓存,我们只需要等待重试或者直接返回客户端失败让用户手动重试。
这是比较简单,也是很常用的一种解决方式。值得注意的是,我们加锁的时候一定要设置过期时间(如redis的expire),否则会有死锁的风险。
2. 提前更新缓存:
上面一种方案,因为用户是有感知的,如果不想影响用户体验,可以进一步优化为对缓存加标,并记录它的过期时间,当我们读取缓存的时候,先判断它是否快到过期时间,如果是则在返回缓存数据的同时,后台异步请求DB重设缓存,更新它的过期时间。而如果我们获取缓存是缓存已经过期,则还需要我们按照上一个方案处理。
3. "永不失效":
在上面情况下,我们进一步思考,如果我们设置缓存的过期时间非常长(比如3天),同时我们对缓存加标记录设置缓存的时间,每次我们读取缓存的时候,拿到设置缓存的时间跟现在的时间做比对,如果相差时间超过10分钟,我们在后台异步更新缓存。这样对于热点数据而言,就相当于缓存“永不失效”了。