缓存与数据库一致性:以Redis为例
常用的3种缓存读写策略
Cache Aside Pattern(旁路缓存模式)
写 :
- 先更新 DB;
- 然后直接删除 cache 。
读 :
- 从 cache 中读取数据,读取到就直接返回;
- cache中读取不到的话,就从 DB 中读取数据返回;
- 再把数据放到 cache 中。
Read/Write Through Pattern(读写穿透)
服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责(Redis 里没有提供 cache 将数据写入DB的功能)实际是在 Cache-Aside Pattern 之上进行了封装,让 cache 服务自己来写入缓存,对客户端是透明的。
写(Write Through):
- 先查 cache,cache 中不存在,直接更新 DB。
- cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)。
读(Read Through):
- 从 cache 中读取数据,读取到就直接返回 。
- 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
Write Behind Pattern(异步缓存写入)
和 Read/Write Through Pattern 很相似,都是由 cache 服务来负责 cache 和 DB 的读写。Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。这种方法下的 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
缓存+数据库的操作顺序
先更新缓存,再更新数据库
业务无法保证两个写操作都成功。当更新缓存成功,更新数据库失败时,数据库中就是旧数据。
先更新数据库,再更新缓存
在多线程并发时,会出现不一致行为。假设请求 A 先操作数据库,请求 B 后操作数据库,但是可能存在请求 B 先写缓存,请求 A 后写缓存的情况,从而导致数据库与缓存之间的数据不一致。
先写数据库,再删除缓存
业务要保证写数据库和删除缓存是一个原子操作,否则写数据成功,删除缓存失败就会出现数据不一致的问题。
请求1从DB读数据A->请求2写更新数据 A 到数据库并删除cache中的A数据->请求1将数据A写入cache
先删缓存, 再写数据库
先删除缓存,再写数据库前,可能另外一个读请求, 在缓存未命中时,从数据库获取到旧值,将其放到缓存。这时缓存中是旧值,数据库是新值。
请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新
保证一致性的解决方案
采用延时双删策略
在写库前后删除缓存中数据,并且给缓存数据设置合理的过期时间, 从而可以保证最终一致性。
写流程如下:
- 先删除缓存,再写数据库
- 休眠一段时间(比如500毫秒)
- 再次删除缓存
防止在写请求删除缓存但还未成功写入数据库后,读请求可能将旧值加载到缓存。确保读请求结束时,写请求可以删除读请求造成的缓存脏数据。
可能存在的问题:
- 休眠一段时间可能会对性能造成影响;
- 在第二次删除缓存失败后,会导致数据不一致,需要业务方实现重试删除机制。
异步更新缓存策略(基于订阅binlog的同步机制)
使用一个异步同步组件,通过解析从库的 binlog 获取数据,并通过消息队列将其串行化写入到缓存。
写流程:
- 先删除缓存,再写数据库
- 额外组件通过解析从库 binlog, 将写操作发送到消息队列
- 缓存从消息队列中消费,更新缓存(订阅DB的数据操作记录binlog,来更新到Redis)
读流程:
- 先从缓存读取(热数据基本都在Redis)
- 如果缓存未命中,从数据库读取,将数据发送到消息队列。
- 缓存从消息队列消费,更新缓存。
一旦DB中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息通过消息队列推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。将并行化的操作串行化,从而解决了并发问题。但是也导致缓存相对主库落后延迟较大。
混合存储优化方案
- 引入版本号
- Redis 中缓存全量的 Key(防缓存穿透,在布隆过滤器的基础上需要增加删除及动态扩展的功能)
- 用户请求统一由缓存服务,请求不会转发给后端数据库(防缓存雪崩)
- 缓存异步持久化到DB
- Redis 限速,未命中缓存时阻塞当前客户端(防缓存击穿与一致性问题)