Redis-缓存一致性
本文最后更新于:1 年前
[TOC]
啥情况会缓存和 DB 不一致
写并发
例如,两个请求,同时先更新 db,后更新 缓存。
a 更新 db 为 20
b 更新 db 为 10
写缓存更新为 10
写缓存更新为 20
读写并发
a 读缓存,没有值
a 读 db 为 10
b 写 db 更新缓存为 20
a 写缓存 更新为 10
概率低,写操作耗时相对较大。
保证一致性
删除缓存
更新 db
再次删除缓存
第一次删除?
如果没有,写 db 成功,删除 redis 失败,脏数据
第二次删除?
读写并发的问题
删除缓存相比较更新缓存,方案更加简单,而且带来的一致性问题也更少。
而一般情况下,如果把缓存的删除动作放到第二步,有一个好处,那就是缓存删除失败的概率还是比较低的,除非是网络问题或者缓存服务器宕机的问题,否则大部分情况都是可以成功的。
比如,如果业务量不大,并发不高的情况,可以选择先更新数据库,后删除缓存的方式,因为这种方案更加简单。
但是,如果是业务量比较大,并发度很高的话,那么建议选择先删除缓存,因为这种方式在引入延迟双删、分布式锁等机制会,会使得整个方案会更加趋近于完美,带来的并发问题更少。当然,也会更复杂。
其实,先操作数据库,后操作缓存,是一种比较典型的设计模式——Cache Aside Pattern。
这种模式的主要方案就是先写数据库,后删缓存,而且缓存的删除是可以在旁路异步执行的。
这种模式的优点就是我们说的,他可以解决”写写并发”导致的数据不一致问题,并且可以大大降低”读写并发”的问题,所以这也是Facebook比较推崇的一种模式。
如何保证数据库和缓存的数据一致性
一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。
串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
Cache Aside Pattern 旁路缓存模式
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,成功后,然后再删除缓存。如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。
在写数据的过程中,可以先删除 cache ,后更新 DB ?
请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 DB 中读取数据->请求 1 再把 DB 中的 A 数据更新。
在写数据的过程中,先更新 DB,后删除 cache 就没有问题了?
理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多
比如请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 不在缓存中的话也有可能产生数据不一致性的问题。这个过程可以简单描述为:
请求 1 从 DB 读数据 A->请求 2 写更新数据 A 到数据库并把删除 cache 中的 A 数据->请求 1 将数据 A 写入 cache。
为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。
其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。
问题
一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
但实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
所以,要么通过 2PC 或是 Paxos 协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而 Facebook 使用了这个降低概率的玩法,因为 2PC 太慢,而 Paxos 太复杂。当然,最好还是为缓存设置上过期时间。
Read/Write Through Pattern(读写穿透)
Read/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。
Write Back 套路,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作飞快无比,因为异步,write backg 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但是,其带来的问题是,数据不是强一致性的,而且可能会丢失。
另外,Write Back 实现逻辑比较复杂,因为他需要 track 有哪数据是被更新了的,需要刷到持久层上。操作系统的 write back 会在仅当这个 cache 需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫 lazy write。