缓存的正确使用方式:从设计思想到 Cache Aside 实战解析

引言

为什么缓存几乎是"必选项"?

在真实业务系统中,我们往往面临三个典型矛盾:

  • 数据访问频繁
  • 数据库性能有限
  • 用户对响应时间极其敏感

如果所有读请求都直接打到数据库:

  • 数据库成为瓶颈
  • 延迟高、吞吐低
  • 扩容成本高

缓存的本质目标只有一句话:

用空间换时间,用不那么强的一致性,换取极致的性能

一、缓存是一种"设计思想",而不是 Redis 本身

很多人一提缓存就想到 Redis,但实际上:

  • Redis 只是缓存的实现
  • 缓存是一种系统设计思想

缓存的核心思想包括:

  • 数据分层
  • 冷热数据分离
  • 降低对慢系统的依赖
  • 允许短暂不一致,换取整体性能

这也是为什么缓存几乎一定会带来一致性问题

二、最经典的缓存模式:Cache Aside(旁路缓存)

1. 什么是 Cache Aside?

Cache Aside,也叫 旁路缓存 ,是目前使用最广泛的缓存模式。

特点是:

  • 应用代码显式控制
  • 缓存与数据库是并列关系
  • 缓存不主动写 DB,DB 也不主动更新缓存

2. 一个最常见的业务场景

以「查询商品信息」为例:

复制代码
getProduct(productId)

三、Cache Aside 的读策略(Read Through 的实现方式)

1. 标准读流程

复制代码
1. 先查缓存
2. 缓存命中 → 直接返回
3. 缓存未命中 → 查数据库
4. 写入缓存
5. 返回结果

2. 示例代码

复制代码
Product getProduct(Long id) {
    Product product = cache.get(id);
    if (product != null) {
        return product;
    }

    product = productMapper.selectById(id);
    if (product != null) {
        cache.set(id, product, 10, TimeUnit.MINUTES);
    }

    return product;
}

这是 99% 系统的真实实现方式

四、Cache Aside 的写策略:为什么"先更新数据库,再删缓存"?

1. 最直觉的写法(错误示范)

复制代码
1. 更新数据库
2. 更新缓存

2. 这个写法的问题在哪?

看一个并发场景:

时间线示意
复制代码
T1:线程A 更新数据库(新值)
T2:线程B 读取缓存(旧值)
T3:线程A 更新缓存
T4:线程B 把旧值写回缓存(覆盖)

最终结果:

缓存中是旧数据,数据库是新数据

这就是典型的 缓存不一致问题

五、正确的 Cache Aside 写策略

1. 标准做法

复制代码
1. 更新数据库
2. 删除缓存

2. 为什么是"删缓存"而不是"更新缓存"?

因为:

  • 删除是幂等的
  • 删除失败可以重试
  • 更新缓存会引入并发覆盖风险

3. 示例代码

复制代码
void updateProduct(Product product) {
    productMapper.update(product);
    cache.delete(product.getId());
}

六、那"先删缓存,再更新数据库"行不行?

1. 看似合理的流程

复制代码
1. 删除缓存
2. 更新数据库

2. 潜在问题:脏读

复制代码
T1:线程A 删除缓存
T2:线程B 读缓存未命中 → 查数据库(旧数据)
T3:线程A 更新数据库
T4:线程B 写缓存(旧数据)

最终:

缓存再次被写入旧数据

3. 为什么"先更新 DB 再删缓存"概率更低?

因为在现实系统中:

  • 数据库写入通常比缓存慢
  • 删除缓存的时间窗口更短
  • 出现极端并发覆盖的概率更低

所以工程实践中,先更新数据库,再删缓存 是主流选择。

七、Cache Aside 依然存在的问题

1. 写入频繁,缓存命中率下降

在写多读少的场景中:

  • 每次写操作都会删缓存
  • 缓存不断失效
  • 读请求频繁回源数据库

表现为:

  • 缓存"形同虚设"
  • DB 压力上升

2. 典型场景

  • 热点配置频繁修改
  • 用户状态高频变更
  • 实时性要求极高的数据

八、Cache Aside 的优化方案

1. 延迟双删(Double Delete)

复制代码
1. 更新数据库
2. 删除缓存
3. 延迟一段时间
4. 再次删除缓存
java 复制代码
updateDB();
cache.delete(key);
Thread.sleep(500);
cache.delete(key);

目的:

  • 清理并发读写带来的脏缓存

2. 写队列 / Binlog 订阅

  • 通过 MQ / Canal
  • 数据库变更异步通知缓存系统
  • 最终一致性

九、读穿 / 写穿 / 写回 策略简介

1. 读穿(Read Through)

  • 缓存未命中
  • 缓存层负责查 DB
  • 应用层无感知

适合:

  • 缓存组件能力强
  • 应用层简单化

2. 写穿(Write Through)

  • 写请求先到缓存
  • 缓存同步写 DB

特点:

  • 一致性好
  • 写延迟高

3. 写回(Write Back)

  • 写只写缓存
  • 异步刷 DB

适合:

  • 对一致性要求不高
  • 写多读少
  • 日志、统计场景

总结:缓存不是"加了就快"

Cache Aside 的使用法则

  • 读多写少:非常适合

  • 写多读少:慎用,需配合优化

  • 一致性要求极高:考虑不缓存或短 TTL

牢记三句话

缓存不是银弹

一致性是成本

设计永远是权衡

相关推荐
算法与双吉汉堡4 小时前
【短链接项目笔记】Day3 用户模块剩余部分
java·redis·后端
直有两条腿4 小时前
【Redis】原理-数据结构
数据结构·数据库·redis
陌路204 小时前
redis缓存雪崩,击穿,穿透
redis·缓存·mybatis
我认不到你4 小时前
自定义注解实现 Redis Stream 消息监听
spring boot·redis
I'm a winner5 小时前
【FreeRTOS实战】互斥锁专题:从理论到STM32应用题
数据库·redis·mysql
gugugu.5 小时前
Redis持久化机制详解(一):RDB全解析
数据库·redis·缓存
陌路206 小时前
redis持久化篇AOF与RDB详解
数据库·redis·缓存
DemonAvenger6 小时前
Redis缓存穿透、击穿与雪崩:从问题剖析到实战解决方案
数据库·redis·性能优化
爱吃KFC的大肥羊7 小时前
Redis持久化详解(一):RDB快照机制深度解析
数据库·redis·缓存