缓存的思考与总结

缓存的思考与总结

新技术或中间的引入,一定是解决了亟待解决的问题或是显著提升了系统性能,并且这种改变所带来的增幅,可以让我们忽略引入新技术所带来的系统复杂性等负面影响。好比是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的缓存记录即可。
相关推荐
PGCCC1 小时前
【PGCCC】Postgresql 缓存替换算法
数据库·缓存·postgresql
《源码好优多》1 小时前
基于Java Springboot出租车管理网站
java·开发语言·spring boot
·云扬·5 小时前
Java IO 与 BIO、NIO、AIO 详解
java·开发语言·笔记·学习·nio·1024程序员节
求积分不加C5 小时前
Spring Boot中使用AOP和反射机制设计一个的幂等注解(两种持久化模式),简单易懂教程
java·spring boot·后端
枫叶_v6 小时前
【SpringBoot】26 实体映射工具(MapStruct)
java·spring boot·后端
东方巴黎~Sunsiny6 小时前
java-图算法
java·开发语言·算法
2401_857617627 小时前
汽车资讯新趋势:Spring Boot技术解读
java·spring boot·后端
小林学习编程7 小时前
从零开始理解Spring Security的认证与授权
java·后端·spring
写bug的羊羊7 小时前
Spring Boot整合Nacos启动时 Failed to rename context [nacos] as [xxx]
java·spring boot·后端
努力的小陈^O^8 小时前
docker学习笔记跟常用命令总结
java·笔记·docker·云原生