缓存问题浅析
认识缓存
由于一直接触基础服务,没有做过业务系统,希望结合之前的经验同时站在业务系统的视角,整理一下对缓存的理解。
web系统中,缓存可以提高页面加载速度,减少对服务器和存储系统的负载。缓存的本质是空间换时间。
这个系统中,分发器先查看请求结果是否在缓存系统中,如果在直接返回结果;不在就需要分配给一个服务器处理,然后将结果放到缓存系统中;
缓存在哪里
平时见过哪些缓存?
客户端缓存:缓存可以在客户端(浏览器)。
CDN缓存:CDN本质也是一种缓存。
web服务器缓存:反向代理提供静态和动态内容;web服务器可以缓存请求的响应内容,无须访问应用服务器。
数据库缓存:sql语句结果缓存,但是可控性较差,很多企业dba关闭该功能;另外也可配置缓存磁盘的分页数据,但是性能差,还是会执行sql,不缓存sql执行结果。
应用缓存:基于内存的memcached和redis是在应用服务和数据库之间的缓存服务。
单机本地缓存:服务读取本地文件,而不去访问远端服务器。guava库的缓存功能。
cpu缓存:cpu多级缓存,弥补处理器和主存的速度差异。
文件系统的缓存:首次操作文件速度会很慢,但是文件系统会做缓存,第二次操作文件就会速度有提升。
缓存的内容
数据库查询语句
将查询语句的哈希值和查询结果缓存。但是有如下的问题:
- 很难删除已经缓存的结果,不好找到哈希值
- 如果语句中的一个field发生变化,则需要删除多条包含这个field的记录
对象级别的缓存
key就是一个对象,对应代码里的一个具体实例或者一个请求。
缓存何时失效
缓存模式(cache aside mode)
关键点:缓存不和数据库直接交互。
失效:应用先从cache读取数据,如果没有,则从数据库中读取,成功后,然后放入cache中。
命中:应用从cache中读取数据,取到后返回
更新:应用程序向数据库中写数据,成功后,再让cache失效
思考:更新时为什么写成功后要让cache失效,而不是写成功后直接更新cache呢?
- 考虑到并发写导致的脏数据问题:线程A写成功后直接更新cache,然后线程B也写数据后更新cache;无法保证先开始写的A一定先更新好cache;从而会导致A把B的在cache中的数据覆盖掉。
思考:更新时为什么不是先让cache失效,然后写数据库呢?
- 考虑并发读和写,线程A写数据前,让缓存失效,同时线程B读发生miss,然后线程B从数据库读取旧数据然后把旧数据加载到cache中。此时A还没来得及更新完数据库;
思考:写成功后让cache失效,一定没并发问题么?
- 不一定,线程A读取cache,然后miss,从数据库读取到a;此时线程B更新数据b,写b到数据库,然后让cache失效;然后线程A进行的load旧数据a;
- 不过这种实际上基本不会出现,因为数据库更新数据时,会????表;而且更新数据动作比读取数据要慢很多。
这种模式的缺点
- 请求的数据如果不在缓存中就需要经过三个步骤来获取数据,这会导致明显的延迟。
- 当一个节点出现故障的时候,它将会被一个新的节点替代,这增加了延迟的时间。
- 应用程序同时控制cache和数据库,以及更新和失效策略;代码复杂度较高。
直写模式(write through)
关键点:更新数据库操作由cache代理,应用程序只操作cache,不用和数据库打交道。在应用程序更新数据的时候更新cache。
失效:应用从cache读取数据,如果没有,cache自己负责从数据库中读取,成功后,自行更新cache,然后返回给应用
命中:应用从cache中读取数据,取到后返回
更新:应用程序向cache中写数据,分两种,1. cache命中,则更新cache,同步去写数据库;2. cache失效,cache负责去写数据库,直接返回(这里cache中只存储被读取过的数据,只是写过,没有读取的数据不会保留)
缺点
- 写入性能较低,需要同步等待
回写模式(Write Behind Caching Pattern)
关键点:更新数据时,只更新缓存,不更新数据库,cache自己会异步批量去刷新数据库。
这样做会导致,数据强一致性无法同时保证,而且可能丢失;总结起来会出现如下缺点
缺点
- 缓存可能在其内容成功存储之前丢失数据。
- 缓存数据和数据库不是强一致的。
- 系统复杂
参考资料
https://coolshell.cn/articles/17416.html
https://en.wikipedia.org/wiki/Cache_(computing)