在修改数据库数据时,对于缓存是选择更新(Update)还是删除(Delete)?
- 更新缓存(不推荐): 每次更新数据库时,也去更新缓存中的值。
- 缺点:
- 并发写冲突: 线程A和线程B同时写入,DB先A后B,缓存先B后A,导致数据不一致。
- 计算开销: 如果写入频繁但读取很少,或者缓存的值需要经过复杂计算才能得到,那么每次写入都更新缓存是资源的浪费。
- 缺点:
- 删除缓存(推荐): 更新数据库时,直接把缓存删掉,等待下一次读取时由读请求去回填缓存(Lazy Loading)。
- 优点: 简单,避免了复杂的计算和部分并发问题。
既然选择了“删除缓存”,那么是先删除缓存,还是先更新数据库?
- 流程:
- 线程 A 删除 Redis 缓存。
- 线程 A 更新 MySQL。
- 问题(高并发下的脏数据):
如果在线程 A 删除缓存后,更新 MySQL 之前,线程 B 进来读取数据。
- 线程 B 发现缓存空,去读 MySQL(此时还是旧数据)。
- 线程 B 把旧数据写入 Redis。
- 线程 A 终于将新数据写入 MySQL。
- 结果: MySQL 是新数据,Redis 是旧数据,出现不一致。
- 流程:
- 线程 A 更新 MySQL。
- 线程 A 删除 Redis 缓存。
- 问题(极端情况下的脏数据):
虽然概率极低,但理论上存在:
- 缓存刚好失效。
- 线程 B 读 MySQL(旧值)。
- 线程 A 更新 MySQL(新值)。
- 线程 A 删除缓存。
- 线程 B 将读到的旧值写入缓存。
- 注意: 这种情况发生的条件是“写数据库”的操作比“读数据库+写缓存”的操作还要快,这在实际系统中几乎不可能,因为写DB通常比读DB慢得多。
- 真正的问题: 第二步删除缓存失败了怎么办? 如果 DB 更新成功,但 Redis 删除失败,那么 Redis 里存的就一直是旧数据。
为了解决上述方案中的边缘Case(特别是删除失败或并发读写),业界有以下几种成熟方案:
如果你非要采用“先删缓存”的策略,或者为了保险起见,可以使用延时双删。
- 流程:
- 删除缓存。
- 更新数据库。
- 休眠一小段时间(比如 500ms)。
- 再次删除缓存。
- 原理: 休眠的时间是为了确保在步骤2执行期间,如果有并发的读请求把脏数据写入了缓存,步骤4可以把这个脏数据再次干掉。
- 缺点: 增加了接口的响应时间(吞吐量降低)。
针对“先更DB,后删缓存”中删除失败的情况。
- 流程:
- 更新数据库。
- 删除缓存。
- 如果删除失败,将要删除的 Key 发送到消息队列(MQ)。
- 消费者监听 MQ,接收到 Key 后不断重试删除操作,直到成功。
- 优点: 实现了重试逻辑,保证最终一定删除。
- 缺点: 侵入业务代码,业务逻辑和重试机制耦合。
这是目前大厂最常用的方案(如使用阿里的 Canal 中间件)。
- 流程:
- 业务代码只操作 MySQL,完全不管 Redis。
- MySQL 更新数据后产生 Binlog。
- Canal 伪装成 MySQL 的从节点,订阅并解析 Binlog。
- Canal 将解析后的数据变更发送到消息队列(Kafka/RabbitMQ)。
- 专门的服务消费 MQ,负责执行 Redis 的删除或更新操作。
- 优点:
- 完全解耦: 业务代码不需要关心缓存一致性。
- 可靠性高: 依赖 MySQL 的 Binlog,只要数据落库,缓存最终一定会被修正。
- 缺点: 系统架构变复杂了,引入了新的中间件。
| 策略 | 适用场景 | 一致性强度 | 复杂度 | 缺点 |
|---|---|---|---|---|
| 先更DB,后删缓存 | 中小型项目、低并发 | 中 | 低 | 极端情况下有脏数据;如果删缓存失败会有问题。 |
| 延时双删 | 读多写少、由于网络原因可能产生脏数据 | 中高 | 中 | 增加请求耗时。 |
| 订阅 Binlog (Canal) | 大型项目、高并发、对一致性要求较高 | 高 (最终一致) | 高 | 架构复杂,引入中间件维护成本。 |
| 强一致性 (读写锁) | 金融级别、绝对不能错 | 强 | 中 | 性能极差,几乎退化为串行,不适合高并发缓存。 |
在回答时,建议遵循以下逻辑:
- 定调: 首先说明,通常我们提到的一致性是指最终一致性。
- 抛出标准方案: 最通用的方案是 Cache Aside Pattern(先更DB,后删缓存)。
- 指出漏洞: 主动指出这个方案存在“删除失败”的风险。
- 给出解决方案:
- 简单方案:利用消息队列进行失败重试。
- 进阶方案(加分项):利用 Binlog + Canal + MQ 进行异步更新,实现业务解耦和高可靠性。
- 补充场景: 如果面试官问“必须要强一致性怎么办”,回答“使用分布式锁(Redisson ReadWriteLock)或者直接不走缓存读 DB”,但要强调这会牺牲性能。