缓存问题

Posted by keys961 on June 11, 2018

很久以前就要记录的,现在补上。

背景

实习时,隔壁项目出现了一些缓存问题,然后我提起了一些兴趣,然后稍微学习记录了一下,这些问题是:

  • 缓存穿透
  • 缓存雪崩
  • 缓存击穿

缓存的基础知识都略过,不清楚可以参考《数据库系统概念》或者《操作系统概念》

缓存穿透

原因:

  • 查询一个不存在的数据,所以缓存不可能命中
  • 不命中则到存储层查询,失去缓存意义

攻击者可通过大量的这种查询来穿透缓存,让存储层挂掉。

解决方法

  1. 布隆过滤器(实际上是一个很长的二进制向量和一系列随机映射函数。可用于检索一个元素是否在一个集合中。优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难),使用足够大的bitmap,存储可能访问的key,一定不存在的数据被这个bitmap拦截
  2. 对空值缓存,但缓存时间较短,如最长5分钟

缓存雪崩

原因:

  • 设置缓存(如A缓存的所有key数据)用了相同过期时间
  • 某一时刻缓存很多key同时失效,查询瞬间到存储层,存储层压力突然增大

解决方法:

  1. 设置随机化的失效时间,如time + random(t)
  2. 对缓存层进行加锁,或者用队列形式,保证没有那么多的线程/进程并发请求到底层存储系统上
  3. 预加载缓存,大量并发请求到来时手动触发缓存加载
  4. 多级缓存,即使上层缓存失效,下级还是有缓存(时间要更长,我们项目里设置成上一级的2倍),可以缓解存储层压力

缓存击穿

原因:

  • 对于某缓存的一些key(如A缓存的特定key),在某些时间点被超高并发访问,成为热点数据
  • 这些数据在某个时间点过期时,恰好在这个时间点有大量并发请求,使得请求大量到达存储层,击垮底层存储层

解决方法

  1. 加锁(互斥锁):

    在缓存失效的时候,不是立即去访问存储层,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去设置一个Mutex Key,当操作返回成功时,再进行存储层访问并回设缓存;否则,就重试整个缓存get(key)的方法(不会访问存储层)

  2. 提前使用互斥锁:

    内部设置一个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时间戳

  3. ”永不过期“策略

    • 物理不过期
    • 把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”不过期
  4. 资源保护

    采用netflix的hystrix,可以做资源的隔离保护主线程池(不是很懂)

    最后进行比较,没有一种是一定好的,参考Reference

解决方案 优点 缺点
简单分布式互斥锁 1. 思路简单 2. 保证一致性 1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险
“提前”使用互斥锁 1. 保证一致性 同上
不过期策略 1. 异步构建缓存,不会阻塞线程池 1. 不保证一致性。2. 代码复杂度增大(每个value都要维护一个timekey)。3. 占用一定的内存空间(每个value都要维护一个timekey)。
资源隔离组件hystrix 1. hystrix技术成熟,有效保证后端。2. hystrix监控强大。 1. 部分访问存在降级策略。