Cache Aside 旁路缓存 \+ 延迟双删

Cache Aside 旁路缓存 + 延迟双删 完整原理与落地文档

1. Cache Aside(旁路缓存)概述

1.1 核心定义

Cache Aside 又称旁路缓存 / 缓存旁置,是业界最通用的缓存设计模式。

核心特征:

  • MySQL 为唯一权威数据源,Redis 仅为临时副本

  • 读写缓存逻辑完全由业务代码手动控制

  • 缓存与数据库无自动联动,解耦度高、性能最好、落地最简单

1.2 读流程(标准、无争议)

  1. 先查询 Redis 缓存

  2. 缓存命中 → 直接返回数据

  3. 缓存未命中 → 查询 MySQL,回写 Redis,再返回数据

读流程无并发一致性问题,所有业务通用。

1.3 写流程(两种方案对比)

❌ 方案一:先删缓存、再更新 DB(严禁使用)

流程:删缓存 → 更新数据库

致命缺陷:高并发下极易产生永久脏缓存

  1. 线程A删除缓存

  2. 线程B查询缓存失效,读取 DB 旧数据写入缓存

  3. 线程A才更新完数据库

结果:缓存长期是旧数据,严重数据不一致。

✅ 方案二:先更新 DB、再删缓存(标准基础写法)

流程:更新数据库 → 删除缓存

优点:绝大多数场景一致性良好

残留漏洞(核心痛点):极端并发会出现脏缓存

漏洞复现条件

  1. 缓存刚好过期

  2. 读线程查询 DB 耗时非常长

  3. 写线程完成「更新DB + 删除缓存」

  4. 读线程最后将旧数据写入 Redis

结果:永久脏缓存,直到 Key 过期。该问题引出延迟双删方案

2. 延迟双删 原理与解决方案

2.1 核心作用

解决「先更新DB后删缓存」模式下,慢查询并发导致的旧数据回填脏缓存问题

2.2 最终标准流程(生产推荐)

  1. 更新 MySQL 数据库

  2. 立即删除一次缓存(即时清理)

  3. 异步延迟等待(根据业务最慢 SQL 耗时配置)

  4. 二次删除缓存(兜底清理滞后回填的旧数据)

2.3 为什么能解决脏数据?

慢查询线程滞后写入的旧缓存,会被延迟第二次删除直接清掉,彻底杜绝永久脏缓存。

2.4 延迟时间配置规范

  • 普通主键查询:500ms

  • 复杂查询/慢SQL:1s~2s

原则:延迟时间 > 业务最大单次DB查询耗时

2.5 两种落地实现方式

方式1:本地定时任务(单机使用)

通过线程池延迟执行删除。

缺点:集群部署、服务重启会丢失延迟任务。

方式2:延迟MQ消息(企业生产最优)

更新完DB+立即删缓存 → 发送延迟消息 → 消费者执行二次删除。

优点:持久化、不丢任务、支持分布式集群。

3. 优缺点总结

3.1 优点

  • 彻底解决高并发极端脏缓存问题

  • 代码侵入极小,性能损耗极低

  • 无需分布式锁,吞吐量高

3.2 缺点

  • 属于最终一致性,延迟窗口内存在短暂脏读

  • 本地延迟任务不适合分布式集群

4. 生产兜底策略(必加)

所有缓存 Key 必须配置过期时间 TTL

作用:即使极端情况漏删缓存,脏数据也会自动过期,形成:

延迟双删主动清理 + TTL 过期兜底 双保险机制

5. 适用场景

✅ 适合 CacheAside + 延迟双删

高读低写、允许短暂不一致:

  • 商品、设备信息、用户资料、配置项、首页数据

❌ 不适合(强一致业务)

库存、订单、金额、账务等强一致性场景,需要:

  • 分布式锁 / 事务

  • Canal Binlog 异步更新缓存

6. 核心伪代码

Plain 复制代码
// 1. 更新数据库
updateDB(data)
// 2. 第一次立即删除缓存
redis.del(cacheKey)
// 3. 异步延迟二次删除(推荐 MQ 延迟消息)
sendDelayDeleteMsg(cacheKey, delayTime = 1s)