以前只是用过Redis作缓存, 缓存和数据库之间怎么舒服怎么来, 今天才发现原来还有缓存模式一说. 下文将从读和写来介绍3种常用缓存读写模式
旁路缓存模式 ( Cache Aside Pattern )
这个模式应该是比较容易想到的, 同时也是比较常用的.
读
- 若缓存存在数据, 则直接返回缓存数据
- 若缓存不存在该数据, 则到数据库进行查询并将结果写入缓存
写
- 更新db记录
- 删除缓存记录
为什么写操作时要先更新db后删除缓存?
我们假设先删除缓存再更新db, 且有一个查询和一个更新操作. 数据库与缓存存在一个记录old.
假设更新操作已经按照我们的假设删除了缓存old, 正准备更新db为new, 此时查询操作开始, 没命中缓存, 于是从db中查询并更新旧数据old到缓存中, 这个时候更新操作才开始进行更新, 将数据库更新为new.
整个流程下来, 两个操作都顺利完成了任务, 此时数据库数据为更新后的new, 而缓存中为旧数据old. 于是就产生了脏数据, 甚至如果接下来没有写操作或者没有给缓存设置过期时间的话, 这个脏数据会一直脏下去.
那么, 先更新db再删除缓存就没有问题吗?
并不是, 先更新db再删除缓存也存在一定问题.
还是一样我们有两个操作: 查询与写入. 数据库与缓存内容为old. 接下来分为两种情况
1 首先假设写入操作已经更新完db为new, 即将删除缓存. 此时查询操作他来了, 拿了缓存中的old就走了, 查询操作走了后写入操作才来得及将缓存删除. 这就导致了此次查询操作查到的是脏数据.
2 假设查询操作没有命中缓存, 到db中查询结果为old后正打算写入缓存, 这个时候更新操作来了, 光速更新db为new后删除缓存. 更新操作结束后, 查询操作才将查询到的old写入缓存, 于是就出现了缓存为old, 数据库为new的脏数据的情况.
既然如此也存在问题, 那为什么还要这样设计呢?
对于上述2种情况 :
1 首先发生两个操作按照假设的进行的情况概率很低, 而且此时的脏数据只会被读取一次, 是一次性的脏数据, 影响不大.
2 同样的按照假设来的概率很低, 首先db(磁盘)的IO速度是远远小于缓存(内存)的. 于是在查询操作查询db后到写入到缓存中的这段时间, 基本上是不足以给更新操作完成所有动作的, 因此发生的概率很小.
引用缓存更新的套路 | 酷 壳 - CoolShell的一句话
在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性能是有冲突的。软件设计从来都是取舍Trade-Off。
为什么写操作只是删除缓存而不是更新缓存?
读写穿透模式 ( Read/Write Through Pattern )
读写穿透模式主要是将缓存作为主体, 将持久化操作交给缓存进行操作, 应用不需要编写持久化操作的代码.
对于应用来说, 缓存以及数据库是透明的, 应用只需要进行查询/写入, 对于不存在缓存中的数据等并不需要进行额外的操作, 例如 : 查询数据时缓存不存在该数据, 由缓存自己从db中查询数据并写入缓存中, 不需要应用自己写入. 这也就减少了应用的代码量, 少了点要操心的事情.
读
- 若命中缓存, 直接返回
- 缓存中无该数据, 向db查询, 由缓存将查询结果更新到缓存中.
写
- 若缓存中存在该数据, 直接更新缓存数据, 并由缓存自动更新db
- 若缓存中无该数据, 直接更新db.
异步缓存模式 ( Write Behind Pattern )
这种模式和读写穿透模式是差不多的, 比较大的区别在于**”同步”与”异步”**.
异步缓存模式采用的是定时将缓存中需要持久化的数据写入到db中.
这样的设计有一个很明显的问题 : 无法做到强一致性. 因为是定时写入db, 一旦出现缓存更新后, 还没来得及写入db就挂掉了, 此时数据就会出现不一致甚至丢失的情况. 但是带来的好处也是很明显 : 提高了IO速度. 因为每次写入数据时, 不需要立刻写入db, 对于单次数据库操作来说是提高了速度的.
- Post title:3种常用的缓存读写策略
- Post author:QBug
- Create time:2021-06-10 01:20:56
- Post link:https://q-bug4.github.io//articles/2021/06/10/1623259256672.html
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.