缓存更新的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缓存方案
-
过期时间 + 并发更新(stupid)
发现缓存过期时, 线程都去获取并更新缓存; 容易引发雪崩
5个工人(线程)去港口取同样Key的货(get),发现货已经过期被扔掉了,这时5个工人各自分别去对岸取新货,然后返回
-
过期时间 + 单例更新
发现缓存过期时, 线程加锁, 一个线程去获取并更新缓存, 其他排队等待
5个工人(线程)去港口取同样Key的货(get),发现货已经过期被扔掉了,那么只需派出一个人去对岸取货,其他四个人在港口等待即可,而不用5个人全去
对比方案1, 2减少了超时时并发访问后端的调用量, 可以防止雪崩
实现方式可能会用到双检锁: Double Check Locking 解决的问题是:当多个线程存在访问临界区企图时,保证了临界区只需要访问一次
-
主动刷新 + 并发更新
Cache中的Key和相应Value虽然有有效时限, 但是过期不自动删掉
-
同步模式: 各线程发现数据过期, 并发去获得更新数据, 并发写入缓存, 如果更新失败, 拿着旧数据返回(不管更新结果, 拿着数据返回)
5个人各自去远程取新货,如果取货失败,则拿着旧货返回
-
异步模式: 各线程发现数据过期, 并发启动新线程获取数据并更新, 同时直接返回旧数据
5个人各自通知5个雇佣工去取新货,5个工人拿着旧货先回
刷新模式的一个好处是永远有数据, 即使后端挂了, 也有旧数据(数据降级); 另一个好处是异步模式下, 大部分线程可以即时返回(命中)
刷新模式的代价: Key-Value一旦放入Cache就不会被清除,每次更新也是新值覆盖旧值
基于刷新的续费模式需要做好监控,不然有可能Cache中的值已经和真实的值相差很远了,应用还以为是新值而使用
-
-
主动刷新 + 单例更新
-
同步模式: 线程发现数据过期, 一个线程去获取并更新缓存, 该线程等待然后返回, 其他线程直接拿到旧数据返回
派一个人去远方港口取新货,其余4个人拿着旧货先回
-
异步模式: 线程发现数据过期, 一个新线程去获取并更新缓存, 所有原始线程拿到旧数据返回
5个人通知一个雇佣工去远方取新货,5个人都拿着旧货先回
4和3比, 需要加锁, 好处是降低了资源开销.
-
-
主动刷新 + 单例更新 + 失败续费(更新失败延长旧缓存的有效时间)
- 同步模式
- 异步模式
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在京东到家的订单中的使用 其中的缓存防穿透和雪崩, 就是利用一批锁来实现限流.