缓存的思考与总结

缓存的思考与总结

新技术或中间的引入,一定是解决了亟待解决的问题或是显著提升了系统性能,并且这种改变所带来的增幅,可以让我们忽略引入新技术所带来的系统复杂性等负面影响。好比是CAP理论中对C和A的权衡。

什么是缓存

从内存中读取数据,从文件系统通过IO读取磁盘数据,两者在时间上存在较大差异,毫无疑问,从内存中读取数据相较于磁盘会更快,于是便有了缓存,很典型的以空间换时间的运用。

如果数据都放在内存中自然是最好不过,也就没有缓存这么一说了。但目前来看,内存依旧属于紧张资源,需要有选择的将数据(读多写少 )放入内存作为缓存来使用。

在缓存方面,大多数问题都可以归结到以下两个方面去讨论:

  • 缓存命中率
  • 数据一致性

缓存命中率

读取数据时,从缓存中是否读取到了数据。系统在设计添加缓存层时,一方面提升系统响应速度,另一方面也能拦截部分查询请求从而分担数据库压力。对于一些我们希望在缓存层被拦截的请求,如果缓存没有命中,那么缓存层将失去意义。比如常说的几个概念:

  • 缓存穿透:查询缓存中不存在的键(非法的key),请求到达DB层
  • 缓存击穿:查询缓存中不存在的键(尚未构建缓存),请求到达DB层
  • 缓存雪崩:缓存大面积失效,请求到达DB层

你会发现,上述都是在描述一个概念:缓存命中率。只不过是不同场景下缓存未命中的情况:

我们希望请求在缓存层被拦截,但由于未命中缓存导致请求到达DB层。所以上述场景的解决方案也都是围绕着:如何提高缓存命中率 来展开的。

缓存击穿还涉及并发场景下缓存重建问题,需要通过加锁来避免。

数据一致性

有了缓存,就意味着有了两份数据,DB层一份,内存一份,那就必然会涉及数据一致性的问题,并且由于是两份数据,数据同步期间必然会存在不一致情况,除非将数据的修改和缓存的修改作为一个原子操作(单体应用)。所以,不能仅仅要设计数据如何读取提高命中率,还要设计数据更新时的策略。也就是说,加入缓存层后,要从读写两方面进行约束,形成闭环,这样才能保证缓存和DB层的数据一致性。

常见的场景必然有成熟的解决方案,对于缓存的数据一致性问题,常见的设计有以下几种:

  • 旁路模式
  • 直写模式
  • 异步写模式

旁路模式 Cache aside

读取时先从缓存中读取数据。如果缓存中没有数据,则从数据库中读取,并将数据写入缓存。更新数据时,先更新数据库,然后再将缓存中的数据失效。

旁路模式中,并发场景下,先更新数据后再删除缓存先删除缓存再更新数据 两者有所不同,即使双删策略保证第二次删除后读取到的都是新数据。

推荐使用先更新数据库再删除缓存 的做法,优点是不存在使用旧数据重建缓存的情况,且数据不一致的窗口期不依赖于第二次删除,也就是说:更新数据后删除缓存前,并发读会读到旧缓存,但更新数据且删除缓存后,不一致窗口期便结束了。假如设定的第二次删除的延时是1小时,先更新数据库再删除缓存 这种方式会在删除缓存后结束数据不一致;但先删除缓存再更新数据的方式则强依赖与第二次删除,会在1小时后才结束数据不一致。

双写模式

顾名思义,既写数据库,又写缓存。包括直写和异步写(严格的双写是指一份数据同时写入两种存储介质,这里的异步写归结到双写模式下便于记忆)

直写模式 write through

读取时先从缓存中读取数据,在写入数据时,数据同时写入缓存和数据库

异步写 Write Behind

写入数据时,数据只更新缓存,并异步批量刷新到数据库中

旁路和双写

为了方便理解和区分旁路和双写模式,最简单的区分就是:

旁路模式中,缓存更新(失效重建)是被动 的,由后续的读操作进行缓存重建;而双写模式则是主动更新缓存

旁路模式和双写模式是保证缓存和DB数据一致性的常用做法。分布式系统中也存在一些在旁路和双写基础上进行改进增强的设计,比如旁路模式+TTL过期时间,双写+补偿机制等,用来处理缓存操作失败时的场景,感兴趣的可以继续研究。

案例

写这篇偏总结性的文章,起因是之前写的一个IDEA插件 ,在第一版设计的时候,考虑到之后插件记录的数据增多,于是通过代理模式预留了扩展。

这里通过代理模式添加缓存层,在代理对象中统一进行缓存的处理。

由于插件开发是单体应用且不考虑多线程场景(多编辑器同时操作一个源文件时,会提示并拒绝),所以这里我们使用旁路模式实现最简单的LRU缓存。具体怎么做呢?

  1. 读操作优先读缓存,缓存没有再读DB(这里是持久化文件)
  2. 数据一旦发生修改,例如修改了高亮记录中的笔记内容,则失效缓存,等待下一次读取时重新构建缓存即可。
    但是有个问题:这里使用的缓存key和value,key是源码文件名称,value是该文件所有的高亮笔记。这里缓存的粒度很大,如果每次修改一条高亮笔记就重建整个源码文件的数据,不是很合理。所以还是双写模式更适合,避免牵一发动全身。每次更新笔记,只更新缓存中笔记列表里面相同ID的缓存记录即可。
相关推荐
火烧屁屁啦21 分钟前
【JavaEE进阶】初始Spring Web MVC
java·spring·java-ee
w_312345434 分钟前
自定义一个maven骨架 | 最佳实践
java·maven·intellij-idea
岁岁岁平安37 分钟前
spring学习(spring-DI(字符串或对象引用注入、集合注入)(XML配置))
java·学习·spring·依赖注入·集合注入·基本数据类型注入·引用数据类型注入
武昌库里写JAVA40 分钟前
Java成长之路(一)--SpringBoot基础学习--SpringBoot代码测试
java·开发语言·spring boot·学习·课程设计
Q_19284999061 小时前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
张国荣家的弟弟1 小时前
【Yonghong 企业日常问题 06】上传的文件不在白名单,修改allow.jar.digest属性添加允许上传的文件SH256值?
java·jar·bi
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
yuanbenshidiaos1 小时前
C++----------函数的调用机制
java·c++·算法
是小崔啊2 小时前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
海海不掉头发2 小时前
苍穹外卖-day05redis 缓存的学习
学习·缓存