分布式缓存

分布式缓存通过 Redis 等系统解决本地缓存无法共享和可扩展性差的问题,典型读流程是先查缓存、未命中再查数据库并写回缓存;同时需要通过缓存空值和布隆过滤器防止缓存穿透,通过随机过期时间和 Redis 高可用防止缓存雪崩,并采用“先更新数据库,再删除缓存”的策略尽量保证缓存与数据库最终一致。

一、核心主线

本节讲的是高并发读场景的第三种通用方案:分布式缓存

本地缓存性能很高,但存在明显限制:

  • 多个服务进程之间无法共享;
  • 与编程语言和进程绑定;
  • 服务进程携带缓存数据后变成有状态服务,可扩展性变差;
  • 服务进程重启后,本地缓存全部丢失。

因此,需要一种支持多进程共享、语言无关、可扩展、可持久化的缓存系统,这就是分布式缓存。

典型链路:

1
2
3
4
5
读请求

查询 Redis 缓存
├─ 命中:直接返回
└─ 未命中:查询数据库 → 写入 Redis → 返回

二、分布式缓存选型

1. Memcached 与 Redis

主流分布式缓存包括:

  • Memcached;
  • Redis。

二者都可以实现缓存数据共享,并且与编程语言无关。

但在互联网应用中,Redis 更常见。

2. Redis 更流行的原因

能力 Memcached Redis
数据类型 主要支持字符串 支持 String、List、Set、Hash、Sorted Set 等
数据持久化 不支持 支持 RDB 和 AOF
高可用 能力较弱 支持主从复制、主从切换
分布式能力 依赖客户端一致性哈希 支持 Redis Cluster,也有 Codis、Twemproxy 等方案

Redis 的优势可以概括为:

  • 数据结构丰富;
  • 可持久化;
  • 高可用能力较强;
  • 可扩展能力较强。

因此,Redis 通常是互联网应用分布式缓存的首选。


三、Redis 缓存基本使用方式

1. 典型读取流程

使用 Redis 缓存时,常见逻辑如下:

1
2
3
4
5
6
7
8
9
查询 Redis
├─ 命中:返回缓存数据
└─ 未命中:

查询数据库

将数据写入 Redis,并设置过期时间

返回数据

这种模式可以让热点数据尽量停留在 Redis 中,减少数据库读请求压力。

2. 为什么要设置过期时间

缓存数据必须设置合适的过期时间。

主要原因有两个。

2.1 避免无效数据长期占用内存

如果缓存数据永不过期,访问频率极低甚至不再访问的数据会一直占用 Redis 内存。

设置过期时间后:

  • 不再访问的数据会自动删除;
  • 经常访问的数据可以通过刷新过期时间继续保留;
  • Redis 内存可以被更有效利用。

2.2 作为数据不一致的兜底机制

数据库和 Redis 之间可能因为故障、并发或更新失败产生短暂不一致。

如果缓存过期时间是 10 秒,那么即使发生不一致,最多也只持续约 10 秒。

1
2
3
4
5
6
7
缓存与数据库不一致

等待缓存过期

重新从数据库加载最新数据

最终一致

过期时间可以帮助系统实现最终一致性兜底。


四、缓存穿透

1. 什么是缓存穿透

缓存穿透指的是:请求访问的数据既不在 Redis 中,也不在数据库中。

流程:

1
2
3
4
5
6
7
8
9
请求非法数据

Redis 未命中

查询数据库

数据库也不存在

返回空

如果黑客持续请求不存在的数据,所有请求都会绕过缓存直接访问数据库,可能导致数据库被压垮。

2. 缓存空值

一种解决方式是:当数据库中也找不到数据时,在 Redis 中缓存一个空值。

1
2
3
4
5
数据库不存在

Redis 缓存空值

后续请求直接被 Redis 拦截

优点:

  • 实现简单;
  • 可以阻止对同一条非法数据的重复数据库访问。

缺点:

  • 如果攻击者请求大量不同非法 Key,会在 Redis 中写入大量空值;
  • 无用空值可能挤掉正常缓存;
  • 缓存命中率下降;
  • 数据库仍然可能面临风险。

3. 布隆过滤器

布隆过滤器适合解决大量非法 Key 导致的缓存穿透。

它由两部分组成:

1
2
3
固定长度为 m 的二进制向量
+
k 个哈希函数

3.1 写入数据

当某个数据加入布隆过滤器:

  1. 使用 k 个哈希函数计算 k 个哈希值;
  2. 每个哈希值对 m 取模;
  3. 将二进制向量中对应的 k 个位置设置为 1。

3.2 查询数据

查询某数据是否存在时:

  1. 使用相同 k 个哈希函数计算位置;
  2. 检查二进制向量对应位置。

判断规则:

检查结果 结论
任意位置为 0 数据一定不存在
所有位置为 1 数据可能存在

布隆过滤器会有“误判存在”的可能,但不会把不存在的数据误判为一定不存在。

4. 布隆过滤器防穿透流程

1
2
3
4
5
6
7
8
9
请求数据

查询 Redis
├─ 命中:返回
└─ 未命中:

查询布隆过滤器
├─ 一定不存在:直接返回空,不查数据库
└─ 可能存在:继续查数据库

如果数据库仍查不到,再在 Redis 中写入空值。

这样可以大幅减少非法请求对数据库的访问。


五、缓存雪崩

1. 什么是缓存雪崩

缓存雪崩指的是:大量缓存数据在同一时间失效,导致请求同时涌向数据库。

1
2
3
4
5
6
7
大量缓存同时过期

Redis 大面积未命中

请求集中访问数据库

数据库被压垮

缓存雪崩和缓存穿透的区别:

问题 原因
缓存穿透 请求的数据不存在,绕过缓存打到数据库
缓存雪崩 大量缓存同时失效,请求集中打到数据库

2. 诱因一:大量数据过期时间相同

如果大量缓存设置了相同过期时间,就可能在某一时刻同时失效。

解决方案:

设置过期时间时增加小范围随机值。

例如:

1
2
3
基础过期时间:10 分钟
随机扰动:0~60 秒
实际过期时间:10~11 分钟

这样可以让缓存过期时间分散,避免同一时刻大面积失效。

3. 诱因二:Redis 服务宕机

如果 Redis 整体不可用,所有读请求都会回源数据库。

解决方案:

  • 使用高可用 Redis 集群架构;
  • 采用主从、哨兵或 Redis Cluster;
  • 降低 Redis 单点故障概率;
  • 必要时配合限流和降级保护数据库。

六、缓存更新问题

1. 为什么缓存更新复杂

当数据库中的数据发生变化时,缓存也需要随之变化。

看似只有两步:

1
2
更新数据库
更新缓存

但实际会遇到很多问题:

  • 先更新数据库还是先更新缓存?
  • 修改缓存数据,还是直接删除缓存?
  • 并发读写是否会导致数据不一致?
  • 第一步成功、第二步失败时怎么办?

本节分析了四种方案。


2. 方案一:先修改缓存,再更新数据库

流程:

1
2
3
修改缓存

更新数据库

问题:

  • 并发写请求可能导致缓存和数据库顺序不一致;
  • 如果缓存修改成功但数据库更新失败,需要回滚缓存;
  • Redis 不支持类似数据库事务的完整回滚能力。

结论:不推荐。


3. 方案二:先更新数据库,再修改缓存

流程:

1
2
3
更新数据库

修改缓存为最新值

问题:

并发写请求可能出现顺序错乱。

例如:

1
2
3
4
请求 A:数据库写 b
请求 B:数据库写 c
请求 B:缓存写 c
请求 A:缓存写 b

结果:

1
2
数据库 = c
缓存 = b

缓存变成旧值。

结论:不推荐。


4. 方案三:先删除缓存,再更新数据库

流程:

1
2
3
删除缓存

更新数据库

问题:

并发读写可能导致旧数据重新写回缓存。

典型过程:

1
2
3
4
5
写请求 A 删除缓存
读请求 B 读缓存未命中
读请求 B 查数据库,读到旧值 a
读请求 B 将旧值 a 写入缓存
写请求 A 更新数据库为 b

结果:

1
2
数据库 = b
缓存 = a

缓存仍然是旧数据。

结论:存在风险,不推荐作为首选。


5. 方案四:先更新数据库,再删除缓存

流程:

1
2
3
更新数据库

删除缓存

这是本节推荐方案。

5.1 为什么推荐删除缓存,而不是修改缓存

删除缓存后,下一次读请求会:

1
2
3
4
5
缓存未命中

读取数据库最新值

重新写入缓存

这样可以避免并发写请求直接把旧值写入缓存。

5.2 并发场景下的优势

无论并发写顺序如何,最后都会删除缓存。

因此,后续读请求要么:

  • 读不到缓存,回源数据库拿最新值;
  • 或者由读请求重新写入最新数据。

这样可以大概率保证缓存和数据库一致。

6. 删除缓存失败怎么办

如果数据库更新成功,但删除缓存失败,可以进行重试。

可选方案:

  1. 写请求中启动异步线程重试删除;
  2. 使用消息队列监听数据库变更并删除缓存;
  3. 依赖缓存过期时间兜底。

更推荐简单做法:

删除缓存失败时执行一次异步重试;如果重试也失败,则依赖缓存过期时间兜底。

原因是:

  • 删除失败概率很低;
  • 为缓存设置过期时间已经可以保证最终一致;
  • 没必要为低概率问题引入过重的系统复杂度。

七、四种缓存更新方案对比

方案 流程 主要问题 是否推荐
先修改缓存,再更新数据库 Cache → DB 数据库失败时缓存回滚困难,并发写不一致 不推荐
先更新数据库,再修改缓存 DB → Cache 并发写可能导致旧值覆盖新值 不推荐
先删除缓存,再更新数据库 Delete Cache → DB 并发读可能把旧数据写回缓存 不推荐首选
先更新数据库,再删除缓存 DB → Delete Cache 删除失败需重试或过期兜底 推荐

推荐方案:

1
2
3
4
5
6
7
更新数据库成功

删除缓存

删除失败则异步重试一次

仍失败则等待缓存过期兜底

八、核心架构思想

  1. 分布式缓存解决了本地缓存无法共享、语言绑定、状态化和重启丢失的问题。
  2. Redis 因为数据类型丰富、可持久化、高可用和分布式能力强,常作为分布式缓存首选。
  3. 缓存读取流程通常是先查缓存,未命中再查数据库,并把结果写回缓存。
  4. 缓存数据应设置过期时间,用于释放无效数据和兜底最终一致性。
  5. 缓存穿透是非法请求绕过缓存直接打到数据库。
  6. 缓存空值可以拦截同一非法 Key,但无法很好应对大量不同非法 Key。
  7. 布隆过滤器适合在访问数据库前判断数据是否一定不存在。
  8. 缓存雪崩是大量缓存同时失效或 Redis 宕机导致请求集中打到数据库。
  9. 随机化过期时间可以避免大量缓存同一时刻过期。
  10. Redis 高可用架构可以降低缓存系统整体不可用的概率。
  11. 缓存更新不能简单地同时修改数据库和缓存,必须考虑并发读写顺序。
  12. 推荐策略是先更新数据库,再删除缓存。
  13. 删除缓存失败时,可以异步重试,并用缓存过期时间兜底。
  14. 分布式缓存设计的核心权衡是性能、可用性、一致性和复杂度。

© 2026 DadaVinCi's Blog

Elegant theme by Shiro · Made by Acris with ❤️