缓存与数据库的一致性

对于读多写少的场景,缓存是提升系统读性能的一个常见技术。而数据系统一旦引入了新的组件,必然会带来数据一致性的问题。这里不考虑强一致性,强一致性带来的性能问题在高并发的场景下是不可接受的,因此这里说的是最终一致性。

对于缓存和数据库一起使用的模式,一般来说有以下几种。其中最常用的应该是 Cache Aside Pattern。

Cache Aside Pattern

这种方案下,需要分别操作缓存和数据库。

  • 对于读操作,一般都是先读缓存,如果命中则返回结果;如果 miss 则去读库,并将结果放入缓存,然后再返回结果。这个没什么争议。
  • 对于写操作,则面临两个问题:
    • 一个是如何更新缓存,是淘汰缓存(让下一个读操作去更新)还是修改缓存;
    • 一个该先更新数据库还是该先更新缓存

更新缓存还是淘汰缓存?

许多文章在分析淘汰还是更新时,举了很多数据不一致的例子。其实更新缓存和淘汰缓存在数据一致性上没有太大区别。对数据一致性有影响的是操作缓存和操作数据库的先后顺序以及并发操作带来的不确定性。更新缓存和淘汰缓存的区别在于成本:

  • 更新缓存的优点会减少miss,但是有两处成本的增加:
    • 有些缓存的值计算较为复杂,可能消耗较大,没必要放在更新操作中;
    • 有些数据可能是写多读少,那么去频繁更新不会被访问的缓存是性能上的浪费,而删除缓存在 Redis 上只是做一个标记,成本很低。
  • 淘汰缓存的优点是简单,而且减少不必要的缓存写入,缺点则是会造成一次 miss。虽然只有一个 miss,如果是热点 key 的话还会引起缓存击穿的问题,可以用分布式锁的方式或主备缓存来解决,不过这是另一个议题。

因此一般选择淘汰缓存。

先更新数据库还是先淘汰缓存?

首先再明确一次,缓存和数据库不能放在一个事务中(即强一致性),这在并发情况下这是不可接受的。

Cache Aside Pattern 的要求是先更新数据库后淘汰缓存,这也是一般大家推荐的。因为即使在单机数据库加 Redis 的情况下,先淘汰缓存都会有不小的概率导致脏数据,比如更新操作 Q1 删除了缓存后,另一个读操作 Q2 先遇到 miss,然后读取了旧数据,此时 Q1 更新了数据库,然后 Q2 更新了缓存这样的场景。因为这样后面的操作都会读脏数据。

当然先更新数据库后淘汰缓存也会出现极端情况,比如缓存更新的套路中提到的“这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。”的场景。

上述情况描述的是单数据库的情况下,数据库和缓存操作都能成功的前提下的取舍。实际场景中还有以下因素需要考虑:

  • 如果更新完了数据库,删除缓存失败了怎么办。
  • 如果数据库是主从结构,就需要考虑主从延迟。

其实考虑了上面这些因素,先淘汰缓存和后淘汰缓存就都不绝对了,因为都需要一些其他机制来进行补充。最简单的机制:缓存设置较短的过期时间。这个适用于并发度不高的场景。最简单并且肯定会达到最终一致性。另一种机制是延迟双删:

  1. 删除缓存
  2. 更新数据库
  3. 睡眠一个特定时间
  4. 再删除一次缓存

这里的特定时间,是需要根据具体情况衡量的,它需要大于主从延迟,加一次读库并写入缓存的时间,这样才能避免没有另一个进程,读到了写入之间的脏数据并更新到了缓存中。

上面延迟双删会让写操作的RT值变高,因此可以将第二次删除改成异步的方式。可以通过发送消息队列,或者订阅 binlog 获取通知的形式,来进行第二次删除。此时会发现就跟之前说的一样,先淘汰还是后淘汰并不是那么绝对了。总结而言,最终可以采取的机制如下:

  1. 删除缓存(可选)
  2. 更新数据库
  3. 发送一条删除缓存消息给删除缓存的服务/或者删除缓存的服务通过订阅binlog得知数据库的变更
  4. 删除缓存的服务拿到数据后,等待一个特定时间后进行删除缓存

Read/Write Through

PatternRead/Write Through 套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。

  • Read Through:Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的。
  • Write Through:Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

Write Behind Caching Pattern

Write Behind 又叫 Write Back。类似 Linux 文件系统的 Page Cache 的算法。在更新数据的时候只更新缓存,不更新数据库,缓存会异步地批量更新数据库。这种方式的优点在于性能极高,缺点在于数据会丢失,如果数据还没来得及更新到数据库服务就挂了,这些操作就丢失了。

异步缓存更新方案

所有的更新操作都走数据库,然后有一个程序异步的从数据库更新到缓存。这个程序可以通过订阅 binlog 的形式来更新缓存。这种方案也有很大的局限性,因为这种方案下只适合数据量较小的,并且大部分数据都能被命中的情况。因为这种方案需要将所有数据都放入缓存中,它没有缓存 miss 时的加载步骤。

参考

https://coolshell.cn/articles/17416.html
https://mp.weixin.qq.com/s/CY4jntpM7VNkBrz1FKRsOw
https://mp.weixin.qq.com/s/aJ33A5O2PUcUOA34kL-Nzw
https://mp.weixin.qq.com/s/4W7vmICGx6a_WX701zxgPQ

posted @ 2022-03-18 00:02  青石向晚  阅读(393)  评论(0编辑  收藏  举报