很久以前就要记录的,现在补上。
背景
实习时,隔壁项目出现了一些缓存问题,然后我提起了一些兴趣,然后稍微学习记录了一下,这些问题是:
- 缓存穿透
- 缓存雪崩
- 缓存击穿
缓存的基础知识都略过,不清楚可以参考《数据库系统概念》或者《操作系统概念》
缓存穿透
原因:
- 查询一个不存在的数据,所以缓存不可能命中
- 不命中则到存储层查询,失去缓存意义
攻击者可通过大量的这种查询来穿透缓存,让存储层挂掉。
解决方法
- 布隆过滤器(实际上是一个很长的二进制向量和一系列随机映射函数。可用于检索一个元素是否在一个集合中。优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难),使用足够大的bitmap,存储可能访问的key,一定不存在的数据被这个bitmap拦截
- 对空值缓存,但缓存时间较短,如最长5分钟
缓存雪崩
原因:
- 设置缓存(如A缓存的所有key数据)用了相同过期时间
- 某一时刻缓存很多key同时失效,查询瞬间到存储层,存储层压力突然增大
解决方法:
- 设置随机化的失效时间,如
time + random(t)
- 对缓存层进行加锁,或者用队列形式,保证没有那么多的线程/进程并发请求到底层存储系统上
- 预加载缓存,大量并发请求到来时手动触发缓存加载
- 多级缓存,即使上层缓存失效,下级还是有缓存(时间要更长,我们项目里设置成上一级的2倍),可以缓解存储层压力
缓存击穿
原因:
- 对于某缓存的一些key(如A缓存的特定key),在某些时间点被超高并发访问,成为热点数据
- 这些数据在某个时间点过期时,恰好在这个时间点有大量并发请求,使得请求大量到达存储层,击垮底层存储层
解决方法
-
加锁(互斥锁):
在缓存失效的时候,不是立即去访问存储层,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的
SETNX
或者Memcache的ADD
)去设置一个Mutex Key,当操作返回成功时,再进行存储层访问并回设缓存;否则,就重试整个缓存get(key)
的方法(不会访问存储层) -
提前使用互斥锁:
内部设置一个
time
时间戳,比实际time_real
小,即time
<time_real
,后者是缓存实际过期的时间戳。当
now
>=time_real
,同1,并设置新的time_real = now + cache_timeout
,同样,更新time
时间戳当
now >= time
, 即使没失效,也执行1的操作,加锁设置缓存,然后设置新的time_real = now + cache_timeout
,同样,更新time
时间戳 -
”永不过期“策略
- 物理不过期
- 把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”不过期
-
资源保护
采用netflix的hystrix,可以做资源的隔离保护主线程池(不是很懂)
最后进行比较,没有一种是一定好的,参考Reference
解决方案 | 优点 | 缺点 |
---|---|---|
简单分布式互斥锁 | 1. 思路简单 2. 保证一致性 | 1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
“提前”使用互斥锁 | 1. 保证一致性 | 同上 |
不过期策略 | 1. 异步构建缓存,不会阻塞线程池 | 1. 不保证一致性。2. 代码复杂度增大(每个value都要维护一个timekey)。3. 占用一定的内存空间(每个value都要维护一个timekey)。 |
资源隔离组件hystrix | 1. hystrix技术成熟,有效保证后端。2. hystrix监控强大。 | 1. 部分访问存在降级策略。 |