缓存实战汇总

千变万化的缓存使用技巧

Posted on September 18, 2016

缓存更新的3种套路

1. Cache Aside Pattern

  • 读miss: 从数据库中取数据,成功后,放到缓存中
  • 读hit: 从cache中取数据返回
  • 更新数据: 先把数据存到数据库中,成功后,再让缓存失效

对于更新数据, 对比以下2钟不同方式:

  • 先删除缓存, 然后更新数据库

    有并发问题, 造成缓存中脏数据; 一读程一写程, 写程的先删掉老缓存, 这时候读程miss, 加载老db数据到缓存, 然后写程把新数据写入db, 此时数据不一致

  • 先写到数据库, 然后写到cache

    两个并发的写操作也可能导致脏数据; A写程是老数据, B写程是新数据, A先发起, 然后B再发起, A写入了db, 然后B写入了db, 然后B写入了cache, A最后写入cache, 造成数据不一致

  • Cache Aside Pattern也有可能出现不一致(概率比较低): 一读程一写程, 读程miss, 从db得到老数据, 写程此时写入新数据到db, 并删除缓存, 然后读程将老数据写入cache

2. Read/Write Through Pattern

可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache

认为cache和db的更新是同步的, 没有并发问题

3. Write Behind Caching Pattern

类似Linux文件系统的Page Cache的算法(TODO)

  • 更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库
  • 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的

代价:

  • 数据不是强一致性的,而且可能会丢失
  • 实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上

缓存后端的服务过载总结

角色:

  • Client系统
  • Cache系统
  • Server系统

Server 过载的三种原因:

  • Cache故障, Client系统流量全部到Server, 造成Server过载
  • Cache故障后恢复, Cache瞬间命中率为0, 相当于Cache被击穿, Server过载
  • Server不可用, Server恢复时, Cache的数据往往已经过期, 所以Client请求全部到Server, Server过载

Client缓存方案

  1. 过期时间 + 并发更新(stupid)

    发现缓存过期时, 线程都去获取并更新缓存; 容易引发雪崩

    5个工人(线程)去港口取同样Key的货(get),发现货已经过期被扔掉了,这时5个工人各自分别去对岸取新货,然后返回

  2. 过期时间 + 单例更新

    发现缓存过期时, 线程加锁, 一个线程去获取并更新缓存, 其他排队等待

    5个工人(线程)去港口取同样Key的货(get),发现货已经过期被扔掉了,那么只需派出一个人去对岸取货,其他四个人在港口等待即可,而不用5个人全去

    对比方案1, 2减少了超时时并发访问后端的调用量, 可以防止雪崩

    实现方式可能会用到双检锁: Double Check Locking 解决的问题是:当多个线程存在访问临界区企图时,保证了临界区只需要访问一次

  3. 主动刷新 + 并发更新

    Cache中的Key和相应Value虽然有有效时限, 但是过期不自动删掉

    • 同步模式: 各线程发现数据过期, 并发去获得更新数据, 并发写入缓存, 如果更新失败, 拿着旧数据返回(不管更新结果, 拿着数据返回)

      5个人各自去远程取新货,如果取货失败,则拿着旧货返回

    • 异步模式: 各线程发现数据过期, 并发启动新线程获取数据并更新, 同时直接返回旧数据

      5个人各自通知5个雇佣工去取新货,5个工人拿着旧货先回

    刷新模式的一个好处是永远有数据, 即使后端挂了, 也有旧数据(数据降级); 另一个好处是异步模式下, 大部分线程可以即时返回(命中)

    刷新模式的代价: Key-Value一旦放入Cache就不会被清除,每次更新也是新值覆盖旧值

    基于刷新的续费模式需要做好监控,不然有可能Cache中的值已经和真实的值相差很远了,应用还以为是新值而使用

  4. 主动刷新 + 单例更新

    • 同步模式: 线程发现数据过期, 一个线程去获取并更新缓存, 该线程等待然后返回, 其他线程直接拿到旧数据返回

      派一个人去远方港口取新货,其余4个人拿着旧货先回

    • 异步模式: 线程发现数据过期, 一个新线程去获取并更新缓存, 所有原始线程拿到旧数据返回

      5个人通知一个雇佣工去远方取新货,5个人都拿着旧货先回

    4和3比, 需要加锁, 好处是降低了资源开销.

  5. 主动刷新 + 单例更新 + 失败续费(更新失败延长旧缓存的有效时间)

    • 同步模式
    • 异步模式

Client方案分析

  • 对于Server, 能够抵抗服务过载(仅针对cache故障恢复和server故障恢复)的最优方案: 刷新+单例+续费
  • 对于Clinet, 能降低线程等待的最优方案: 各种异步模式

对于cache故障的情况, 如果要解决server过载, 可选方案:

  • Client不请求Server, 打印日志返回默认值
  • Client按照一定概率访问Server
  • Client检测Server运行情况, 视情况访问

Server端方案

  • 流量控制
  • 服务降级
  • 动态扩容

缓存更新逻辑比较:

  • cache then db or db then cache ?
  • update cache or delete cache ?

具体见 缓存使用总结


缓存常见问题

缓存穿透

太多cache miss打到db

  • 解决方案: 在缓存中存储空对象

    代价: 牺牲加大的缓存在保存空对象数据

    适应场景: 少数miss的key, 但是miss次数非常多. 这样通过牺牲少量的内存就可解决穿透问题

雪崩

缓存miss或者失效, 热点数据过期瞬间等, 导致流量瞬间打挂后端服务

  • 解决方案: 很多, 如单例更新, 限流, 降级等

Redis在京东到家的订单中的使用 其中的缓存防穿透和雪崩, 就是利用一批锁来实现限流.


参考