一、缓存穿透:
缓存穿透是指查询一个不一定存在的数据,由于缓存是不命中时需要从数据库查询,查不到的数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库中去查询,造成缓存穿透。
解决办法:
1 . 布隆过滤
对所有可能查询的参数以hash形式存储,在控制才层先进性校验,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够强大bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
补充:Bloom filter
适用范围:可以用来实现数据字典进行数据的判重,或者集合求交集
基本原理及要点:对于原理来说很简单,位数组+k个独立hash函数,将hash函数的对应的值的位数组置1,查找时如果发现所有hash函数对应我位都是1说明存在没很明显这个过程并不保证查找的结果是100%正确的,同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。添加时增加计数器,删除时减少计数器。
二、缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。
这个没有完美的解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。大多数系统设计者考虑加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请i去落到底层存储系如上。
解决办法:
1 . 加锁排队. 限流- 限流算法 1. 计数 2. 滑动窗口 3. 令牌桶 Token Bucket 4. 漏桶 leaky Bucket
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值位空),不是立即去load db,而是使用缓存工具的某些带陈工操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set,mutex key,当操作返回成功时,在进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX . 是 [ SET if Not eXists ] 的缩写,也就是只有不存在的时候才设置,可以利用他来实现锁的效果。
2 . 数据预热
可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期期间,让缓存失效的时间点尽量均匀。
3 . 做二级缓存或者双缓存策略
A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2 , A1缓存失效时间设置为短期,A2为长期。
4 . 缓存永远不过期
这里的 ” 永远不过期 “ 包含两层意思:
(1)从缓存上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是 ” 物理 “ 不过期。
(2)从功能上看,如果不过其,那不就成静态的了吗?,所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是 “ 逻辑 ” 过期。
从实战来看,这种方法对性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受
三、缓存击穿
大量用户数据在redis缓存失效的瞬间打到一个热点的key上,但是redis缓存已经失效,所以到数据库中查询,导致崩溃。
解决方法:
1 . 缓存永不过期(不推荐)
2 . 互斥锁:在大量用户访问redis的时候,使用互斥锁,此时只有一个线程能访问到redis,其他线程开启睡眠,一个线程进入数据库中查询,查询后返回给redis,与此同时剩下的线程结束了睡眠状态,这样的话大量的用户资源去抢占redis,而不是数据库。
3 . 分布式锁