Redis 缓存穿透、缓存击穿、缓存雪崩 等经典问题解读
由于基本看完了 《Redis 设计与实现》中的单机部分内容,所以就可以看一些面试常常会问到的相关问题,带着问题去学习,这样效率会更高。
缓存穿透
简介
缓存穿透(缓存击穿) 表示恶意用户请求很多不存在的数据,由于数据库中都没有,缓存中肯定也没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。
解决方案
1:缓存空值
之所以发生穿透,就是因为缓存中没有存储这些空数据的 key。从而导致每次查询都到数据库去了。那么我们就可以为这些 key 对于的值设置为 null 丢到缓存里面去。后面再查询这个 key 的请求的时候,直接返回 null。这样就不用到数据库中去走一圈了,但是别忘了设置过期时间。
2:布隆过滤器
BloomFilter 类似于一个 hash set , 用来判断某个元素 (Key) 是否存在于某个集合中,这种方案可以加在第一种方案中,在缓存之前在加一层 BloomFilter, 在查询的时候先去 BloomFilter 去查询 Key 是否存在,如果不存在就直接返回,存在再走 查缓存--->查 DB 的流程。
方案选择
特点:Key 比较多,请求重复率低:
针对一些恶意攻击,攻击带过来的大量 Key 是不存在的,那么我们采用第一种方案就会缓存大量不存在Key 的数据。所以采用第二种方案;
特点:空数据的 Key 有限,重复率比较高:
可采用第一种方案;
缓存击穿
简介
在高并发的系统中,大量的请求同时查询一个 Key 时,此时这个 Key 正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们成为缓存击穿。这将导致某一时刻数据库请求量过大,压力剧增。
解决方案
上面的现象是多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其它线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的进程进来发现已经有缓存了,就直接走缓存。
func get(Key string) string { value := redis.get(Key) if(value == null) { // 如果缓存没命中 // 设置 3min 超时,防止 del 失败,导致后续无法从 DB 中 load 数据 if(redis.setnx(key_mutex, 1, 3*60) == 1) { // 如果不存在则设置,单线程操作, 可以充当互斥锁 value = db.get(Key) // 从 DB 中取出对于数据 redis.set(key, value, expire_secs) // 缓存下来 redis.del(key_mutex) // 删除 } else { // 其它线程进入 sleep sleep(50) get(Key) } } else { return value } }
缓存雪崩
简介
缓存雪崩的情况是说,当某一时刻发生大规模的缓存失效的情况,比如你的缓存服务宕机了,大量键过期(失效),接下来的一大波请求瞬间都落在了数据库中导致链接异常。
解决方案
1:加锁
与缓存击穿解决方式一样,采用加锁的方式来解决;
2:建立备份缓存
缓存A和缓存B, A设置超时时间, B不设置超时时间,先从 A读缓存,A没有读B,并且更新 A缓存和B缓存;
3:散开缓存失效时间
我们可以在原因的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件。
4:使用 Hystrix 进行限流 & 降级,比如一秒来了 5000 个请求,我们可以设置假设只能有一秒 2000 个请求能通过这个组件,那么其它剩余的 3000 个请求就会走限流逻辑。然后去调用我们自己
开发的降级组件(降级),比如设置的一些默认值之类的。以此来保证我们的 DB 不会被大量的请求打死。
双写一致性问题
简介
在引入缓存系统的项目中,当我们需要对旧数据进行更新操作时,常常会发生缓存中的数据和数据库中的数据不一致的问题,我们通常采取的策略有以下几种:
1: 先更新数据库,再更新缓存
2: 先更新缓存,再更新数据库
3: 先删除缓存,再更新数据库
解决方案
方案一:这种方案,在大多数场景种不合适,主要原因有:
资源浪费:我们引入缓存主要是对热点数据进行缓存,这时候如果很多用户对于冷数据进行更新,那么我们就没必要去更新缓存,这会导致缓存资源的大量浪费
脏数据:请求 A 更新了数据库;请求 B 更新了数据库;请求 B 更新了缓存;请求 A 更新了缓存,这种情况会出现 A 数据覆盖 B 数据的情况,就会产生脏数据
方案二:这种策略比较多平台在使用,如:Facebook, 但这种策略也存在一些问题,如:
脏数据:造成脏数据的原因主要是由并发引起
方案三:这种策略也有比较多平台在使用,和方案二相同,也会产生脏数据
注:可引入消息系统来避免脏数据(未研究过消息系统,暂时不做分析)
并发竞争问题
简介
Redis 的并发竞争问题,主要是发生在并发写操作,比如现在想把 price 的值进行 +10 操作,两个连接同时对 price 进行写操作,最终结果应该是 30 才正确:
T1: 连接1 将 price 读出, 目标设置的数据为 10 + 10 = 20
T2: 连接2 也将数据读出,也是为 10, 目标设置为 20
T3: 连接1 将 price 设置为 20
T4: 连接2 也将 price 设置为 20,则最终结果是一个错误的 20
解决方案
方案一:可以采用独占锁的方式,类似于操作系统 mutex 机制,不过成本较高
方案二:可以采用乐观锁的方式,成本低,非阻塞,性能高;Redis 提供了 watch 命令,它本质上就是一个乐观锁,实现伪代码:
// redis 伪代码 watch price value = redis.get(price) value = value + 10 multi set(price, value) exec
注:上述操作只有一个能成功,其它都会失败,如果期望有多个成功,则可以把命令入队,然后用一个消费者线程从队头依次取出请求,并做相应操作。
参考资料: