讨论Redis缓存下可能出现的缓存穿透、缓存击穿、缓存雪崩等问题。

另外记录一下缓存预热、缓存更新模式、缓存降级等等。

 

涉及秒杀、抢购、页面瞬间大量访问的情况使用SQL数据库会因为磁盘读写效率问题导致严重的性能弊端。

这时候就需要引入NoSQL技术,比如Redis,它是一种基于内存的数据库,并且提供一定持久化功能。

这里我们讨论Redis的缓存穿透、缓存击穿、缓存雪崩这三个问题。

 

缓存穿透

Redis是一个Key-Value的缓存,key对应的数据在缓存中不存在,则会去请求数据库。

如果大量请求从缓存获取不到,都会请求SQL数据库,从而可能压垮数据库。

比如用一个不存在的用户id来获取用户信息,此时不论是缓存还是数据库都查询不到,若黑客利用此漏洞进行攻击可能压垮我们的数据库。

由于缓存是不命中时被动写的,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

解决方案:

1. 布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合中。

它的优点是空间效率和查询效率极高,缺点是有一定的误识别率和删除困难,但是没有识别错误的情形。

误识别的情况可能是:布隆过滤器报告元素在集合中,但实际上集合中没有。

但是如果布隆过滤器报告元素不在集合中,那么集合中一定没有。

应用举例:Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的IO次数。

 

另外提一句,有一个布谷过滤器(Cuckoo Filter)可以解决布隆过滤器无法删除的缺点。

 

2. 写一个无效值到缓存中

如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存起来。

但它的过期时间会很短,最长不超过五分钟。

这样下次查询依旧会返回一个空值。

 

 

缓存击穿

设置了过期时间的key,它可能会在某些时间点被超高并发访问,这种就属于热点数据。

对应的情况可能是这样:缓存在某个时间点过期,恰好此时针对这个key有大量的请求过来,然后缓存查询不到,这些请求就打在了数据库上,最终将数据库压垮。

解决方案:

1. 使用互斥锁

简单来讲就是缓存查询不到的情况下,不会直接去请求数据库,而是先使用Redis的SETNX(或者Memcache的ADD)设置一个mutex key,当操作返回成功的情况下才请求数据库,否则就重试get缓存方法。

代码如下:

存在的问题是,一个线程去请求数据,其他线程因为拿不到mutex而阻塞,性能会大大降低。

 

2. 设置永不过期

从Redis来说就是功能上的修改,以前有过期时间,改成没有过期时间。

 

3. 异步更新

给数据手动加上一个过期时间,当我们请求数据时,判断数据是否已经过期。

如果数据过期了,则拉起一个线程(协程)异步更新数据。

此时其他线程获取的还是过期的老数据,不过对数据实时性要求不高的数据来说…也是可以的。

 

 

缓存雪崩

我们给缓存的key设置了相同的过期时间,导致在某一时间它们同时失效,请求就全部打到数据库,导致数据库压力过大崩溃。

解决方案

1. 随机过期时间

给热点key加上随机的过期时间,让它们不会同时失效。

 

2. 永不过期

还是那句话,不过期就没这个问题了嘛。

 

3. 速率限制

这里主要是针对数据库,不让大量的请求直接打上去,但是实际上治标不治本。

 

 

缓存预热

当系统上线时,缓存内是没有数据的,如果直接使用的话,大量请求就会直接打在数据库上,说不定上线就宕机。

所以一种可行的操作是:上线前先将数据库内的热点数据缓存到Redis中。

比较通用的方式是:写一个批处理任务,在启动项目时或定时触发将底层数据库内热点数据加载到缓存中。

 

 

缓存更新模式

缓存服务(Redis)和数据服务(MySQL)是相互独立的系统,在更新缓存时或更新数据时无法做到原子性的同时更新两边的数据。因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。

这里讨论如何解决并发场景下更新操作的一致。

更新缓存的模式:

1. Cache Aside 模式:

查询操作:先查询缓存,缓存内没有则查询数据库然后加载到缓存中。

更新操作:先更新数据库,然后让缓存失效或者更新缓存。

 

更新缓存可能导致的脏数据问题:

a,b两个线程,线程执行顺序:

a更新db,b更新db,b线程更新cache,a线程更新cache。

此时数据库存储的是b线程数据,cache存储是a线程的数据,所以缓存存储的是脏数据。

 

让缓存失效可能导致的脏数据问题:

a,b线程,线程执行顺序:

  1. a读取cache,发现没有数据,读db数据(且成功)。
  2. b线程更新db,并让cache失效。
  3. a线程已经读取到数据(b更新前的数据),此时a线程把数据同步到cache中。

此时数据库中的数据是b现场数据,缓存中数据是a线程读取的旧数据,所以也是脏数据。

 

不过让缓存失效出现脏数据的概率非常低:

  1. 这个条件需要发生在读缓存失效,且并发写操作的情况下。
  2. 数据库的写操作比读操作慢得多,而且还要锁表。
  3. 读操作必须在写操作之前进入数据库操作,又要晚于写操作更新缓存。

以上条件发生的概率并不大,但是最好还是为缓存设置一个过期时间。

Facebook就使用了让缓存失效的方法。

 

2. Read/Write Through 模式:

Read Through: 在查询操作中更新缓存。

当缓存失效时(缓存过期或LRU换出),Read Through则用缓存服务器自己加载数据到缓存中。

 

Write Through: 在更新操作中更新缓存。

没有命中缓存,则直接更新数据库,然后返回。

命中缓存,则更新缓存,再由Cache自己更新数据库(同步操作)

 

3. Write Behind Caching 模式:

又叫Write Back,就是Linux文件系统的Page Cache的算法。

更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。

好处是数据的I/O操作飞快无比(因为直接操作内存 ),因为异步,还可以合并对同一个数据的多次操作,所以性能大大提高。

带来的问题:

  1. 数据不是强一致性,而且可能会导致数据丢失
  2. Write Back实现逻辑复杂,因为它要知道哪些数据是需要写到数据库的。

操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

 

 

缓存本身就是通过牺牲强一致性来提高性能,因此使用缓存提升性能,就会有数据更新的延迟性。

需要我们在评估需求和设计阶段根据实际场景去做权衡了。

 

 

缓存降级

缓存降级指的是当访问量剧增、服务出现问题(比如响应过慢或不响应)或非核心业务影响到核心业务的性能时,即使有损部分其他服务,仍要保证主服务可用。

可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。

降级的目的是保证核心服务即使有损依旧可用。

降级可以根据实时监控数据自动降级,也可以配置开关人工降级。

是否需要降级、哪些服务需要降级、是什么情况下降级都取决于系统功能的取舍。

 

举个例子:阿里双十一淘宝购物车无法修改地址(只能使用默认地址),就是被降级了,保证了下单可以提交和付款。

 

【Redis】缓存相关
Tagged on:
0 0 vote
Article Rating
订阅
提醒
0 评论
Inline Feedbacks
View all comments