Redis原理和高可用

Redis原理和高可用

这篇是01-Redis基础.md之后的进阶篇。基础篇解决“Redis怎么用”,这一篇重点解决“Redis为什么这样工作,以及放进真实后端系统后怎么保证可用性、性能和一致性”。

Redis在生产环境里通常不是孤立存在的。它一边承接高频读写、缓存、限流、排行榜、地理位置粗筛等场景,另一边又要和PostgreSQL/MySQL、MQ、CDC、监控系统协同工作。

1
2
3
4
5
业务服务
-> Redis:缓存、锁、计数器、排行榜、热点数据
-> DB:业务事实、事务、审计、复杂查询
-> MQ / CDC:异步补偿、事件传播、缓存对齐
-> 监控系统:慢查询、内存、延迟、大Key、热Key
flowchart LR
    A["业务服务"] --> B["Redis"]
    A --> C["PostgreSQL / MySQL"]
    C --> D["WAL / Binlog"]
    D --> E["CDC: Debezium"]
    E --> F["Kafka / MQ"]
    F --> G["缓存同步消费者"]
    G --> B
    B --> H["监控: slowlog / memory / latency"]

4. 底层原理、高可用架构与数据流转

4.1 持久化机制:数据不丢失的秘密

Redis主要以内存工作,但它不是只能“断电即丢”。Redis提供了RDB、AOF,以及二者结合的混合持久化方案。

先明确一个底线:持久化不是绝对不丢数据,而是在性能、恢复速度、磁盘成本和可接受丢失窗口之间做取舍

方案 核心思想 恢复速度 数据丢失窗口 文件体积 适合场景
RDB 定期生成内存快照 取决于快照间隔 备份、灾难恢复、快速重启
AOF 记录每条写命令 较慢 取决于appendfsync策略 大,需要重写 更低数据丢失窗口
混合持久化 AOF文件前半段是RDB,后半段是增量AOF 较快 接近AOF 较折中 Redis 4.0+常用方案

4.2 RDB快照

RDB会在某个时间点把Redis内存中的数据生成一个紧凑的二进制快照文件,通常是dump.rdb

触发机制

常见触发方式:

1
2
SAVE
BGSAVE

区别:

命令 行为 风险
SAVE 主线程同步保存快照 会阻塞Redis,不适合生产高峰
BGSAVE fork子进程后台保存快照 主线程短暂fork,子进程写磁盘

配置自动快照:

1
2
3
save 900 1
save 300 10
save 60 10000

含义大致是:

  • 900秒内至少1次写入,触发快照;
  • 300秒内至少10次写入,触发快照;
  • 60秒内至少10000次写入,触发快照。

BGSAVE背后的fork与写时复制

BGSAVE通常会fork出一个子进程,由子进程把快照写到磁盘。fork之后,父进程继续处理请求。Linux通过Copy-On-Write减少复制成本:父子进程先共享内存页,当父进程修改某个页面时,内核才复制这个页面。

1
2
3
4
5
Redis主进程
-> fork子进程
-> 子进程写RDB
-> 主进程继续响应请求
-> 写入发生时触发Copy-On-Write

这带来两个实践影响:

  • 内存压力高时,BGSAVE可能因为fork或COW开销导致延迟抖动;
  • 写入很密集时,COW会消耗额外内存,不能把机器内存打满后再指望快照稳定运行。

RDB优缺点

优点:

  • 文件紧凑,适合备份;
  • 重启加载速度通常比AOF快;
  • 对主线程影响较小,主要压力在fork和磁盘写入;
  • 适合做定期全量快照。

缺点:

  • 两次快照之间的数据可能丢失;
  • fork在大内存实例上可能产生明显延迟;
  • 无法像AOF一样按每条写命令追踪变更。

4.3 AOF追加日志

AOF是Append Only File。它把每条写命令追加到日志文件中,Redis重启时重新执行这些命令恢复数据。

开启AOF:

1
2
appendonly yes
appendfilename "appendonly.aof"

也可以在线开启:

1
redis-cli config set appendonly yes

但在线修改后要记得写回配置文件或执行合适的配置重写,否则重启后配置可能丢失。

appendfsync写入策略

AOF最关键的配置是:

1
appendfsync everysec

三种策略:

策略 含义 性能 数据安全
always 每次写命令都fsync 最慢 最安全
everysec 每秒fsync一次 折中 最多可能丢约1秒数据
no 交给操作系统决定何时刷盘 最快 丢失窗口不可控

生产中常见选择是everysec。它不是绝对不丢数据,但在性能和可靠性之间比较均衡。

AOF重写

AOF会不断追加,文件会越来越大。例如:

1
2
3
INCR counter
INCR counter
INCR counter

最终状态可能只是:

1
SET counter 3

AOF重写就是把“冗余历史命令”压缩成“恢复当前状态所需的最小命令集合”。

需要注意版本差异:Redis 7.0之后使用multi-part AOF机制,AOF不再只是一个单文件模型,而是由base file、incremental files和manifest共同管理。理解时仍可以按“基础状态 + 增量写入”这个模型来把握,运维备份时则要按实际版本的文件组织来处理。

手动触发:

1
BGREWRITEAOF

常见配置:

1
2
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

含义是:当AOF文件比上次重写后增长到一定比例,并且超过最小体积后,自动触发重写。

AOF优缺点

优点:

  • 数据丢失窗口更小;
  • 追加写文件,符合磁盘顺序写特性;
  • AOF文本协议可读性比RDB更好,排查时更直观。

缺点:

  • 文件通常比RDB大;
  • 重放AOF恢复速度可能慢于RDB;
  • always策略会显著影响写入性能;
  • 重写期间也有fork、COW和磁盘压力。

4.4 混合持久化

Redis 4.0之后支持AOF混合持久化,常见配置:

1
2
appendonly yes
aof-use-rdb-preamble yes

它的思路是:

1
2
3
AOF逻辑内容
-> 基础部分:RDB格式的全量快照
-> 增量部分:AOF格式的写命令

好处:

  • 恢复时先快速加载RDB部分;
  • 再重放少量AOF增量;
  • 兼顾RDB恢复速度和AOF较小丢失窗口。

实践建议:

  • 只做纯缓存且允许丢失:可以关闭持久化,但要有缓存预热和降级方案;
  • 做会话、排行榜、计数等较重要数据:优先考虑AOF everysec或混合持久化;
  • 真正业务事实:不要只放Redis,最终仍应落到DB或其他可靠存储。

4.5 内存管理与淘汰策略

Redis是内存数据库,maxmemory是生产环境必须认真配置的参数。

1
2
maxmemory 8gb
maxmemory-policy allkeys-lru

当Redis内存达到maxmemory后,会根据maxmemory-policy决定怎么处理新写入。

常见淘汰策略

策略 含义 适合场景
noeviction 不淘汰,写入可能返回错误 Redis存的是重要数据,不允许自动淘汰
allkeys-lru 从所有key中淘汰最近最少使用的key 通用缓存
volatile-lru 只从设置了TTL的key中按LRU淘汰 混合数据,只希望淘汰缓存key
allkeys-lfu 从所有key中淘汰最不经常使用的key 热点相对稳定的缓存
volatile-lfu 只从设置了TTL的key中按LFU淘汰 设置TTL的热点缓存
allkeys-random 从所有key中随机淘汰 访问分布接近随机
volatile-random 从设置TTL的key中随机淘汰 简单TTL缓存
volatile-ttl 优先淘汰剩余TTL短的key 希望快过期的key先被淘汰

LRU与LFU怎么选

LRU看“最近有没有用过”,LFU看“过去一段时间用得频不频繁”。

1
2
LRU: 最近少用 -> 淘汰
LFU: 频次少用 -> 淘汰

选择建议:

  • 访问热点变化快:allkeys-lru更灵活;
  • 热点长期稳定:allkeys-lfu更能保住真正热Key;
  • 同一个实例里既有缓存又有不可淘汰数据:优先用volatile-*,并确保缓存key设置TTL;
  • Redis只做缓存:allkeys-lruallkeys-lfu更直接;
  • 不允许Redis自行丢key:noeviction,但业务必须处理写入失败。

注意:Redis的LRU/LFU是近似算法,不是每次都全量扫描所有key。这样做是为了在性能和淘汰效果之间取得平衡。

4.6 高可用架构演进

Redis高可用通常按下面路径演进:

1
2
3
4
单实例
-> 主从复制:解决读扩展和基础冗余
-> Sentinel:解决自动故障转移
-> Cluster:解决单机容量和写入瓶颈

4.7 主从复制:Primary-Replica

传统资料常叫Master-Slave,现在更推荐叫Primary-Replica。

核心模式:

1
2
3
4
5
6
7
client write
-> primary
-> replica 1
-> replica 2

client read
-> primary 或 replica

配置从节点:

1
replicaof 10.0.0.1 6379

或运行时执行:

1
REPLICAOF 10.0.0.1 6379

主从复制解决的问题:

  • 主节点数据有副本;
  • 读请求可以分摊到副本;
  • 故障切换时有候选节点;
  • 备份和部分分析读取可以从副本执行。

但它不等于完整高可用:

  • 主节点宕机后,不会自动完成故障切换;
  • 复制通常是异步的,主节点刚写完,从节点可能还没同步;
  • 读从节点可能读到旧数据;
  • 主节点宕机时,未复制到副本的数据可能丢失。

读写分离要特别注意“读己之写”。如果用户刚更新个人资料,下一次请求却读了副本,可能看到旧数据。解决思路包括:

  • 写后短时间读主;
  • 对关键读强制读主;
  • 使用版本号或更新时间判断;
  • 接受短暂最终一致,但前端和业务文案要有预期。

4.8 Sentinel哨兵模式

Sentinel用于监控Redis主从,并在主节点故障时自动选举新主节点。

典型结构:

1
2
3
4
5
6
sentinel-1   sentinel-2   sentinel-3
\ | /
\ | /
primary redis
/ \
replica-1 replica-2

Sentinel主要职责:

  • 监控primary和replica是否可达;
  • 判断主节点是否下线;
  • 多个Sentinel达成一致后执行故障转移;
  • 从replica中选出新primary;
  • 通知客户端新的主节点地址。

关键概念:

概念 含义
主观下线 单个Sentinel认为某节点不可达
客观下线 足够多Sentinel都认为主节点不可达
quorum 判断客观下线所需的票数
failover 选新主、重配从节点、更新拓扑

实践建议:

  • Sentinel至少部署3个,避免单点和误判;
  • Sentinel本身要分散在不同机器或故障域;
  • 客户端要支持Sentinel发现,不要写死Redis主节点IP;
  • Sentinel解决自动故障切换,不解决单机容量瓶颈;
  • 故障转移期间可能有短暂不可写和数据丢失窗口。

4.9 Redis Cluster集群模式

Redis Cluster用于解决单机内存容量和单节点写入瓶颈。

它把整个key空间划分为16384个哈希槽:

1
slot = CRC16(key) % 16384

每个主节点负责一部分槽:

1
2
3
node-1: slot 0     - 5460
node-2: slot 5461 - 10922
node-3: slot 10923 - 16383
flowchart LR
    A["client"] --> B{"key -> hash slot"}
    B --> C["node-1: slots 0-5460"]
    B --> D["node-2: slots 5461-10922"]
    B --> E["node-3: slots 10923-16383"]
    C --> C1["replica"]
    D --> D1["replica"]
    E --> E1["replica"]

如果客户端访问了错误节点,Redis会返回重定向:

1
2
GET x
-MOVED 3999 127.0.0.1:6381

成熟客户端会根据MOVED更新本地槽位缓存,并把请求发到正确节点。

Hash Tag与多key操作

Cluster中,多key命令要求key位于同一个slot。可以用Hash Tag控制槽位:

1
2
3
order:{1001}:info
order:{1001}:items
order:{1001}:lock

Redis只计算{1001}这部分的hash,因此这些key会落在同一个slot。

Cluster适合什么

适合:

  • 单机内存放不下;
  • 单节点写吞吐不够;
  • key天然可分片;
  • 可以接受Cluster客户端、重定向和运维复杂度。

不适合:

  • 大量跨key、跨slot事务;
  • 依赖多个DB编号隔离业务;
  • 运维能力不足但强行上集群;
  • 数据模型还没稳定就急着分片。

Cluster的本质不是“更高级的单机Redis”,而是把数据分散到多个主节点上。它提升容量和吞吐,也带来槽位迁移、客户端路由、跨slot限制、故障恢复等复杂度。

4.10 异构数据同步:CDC流式架构

缓存一致性的一个难点是:业务代码更新数据库后,还要记得更新或删除缓存。随着服务变多,缓存逻辑很容易散落在各个代码路径中。

CDC,Change Data Capture,变更数据捕获,是另一种思路:数据库仍然是事实源,系统从数据库日志里捕获已提交变更,再异步同步到Redis

4.11 Debezium监听PostgreSQL WAL同步Redis

PostgreSQL的WAL,Write-Ahead Logging,是数据库记录变更的日志。Debezium PostgreSQL connector可以基于逻辑复制捕获行级变更:首次连接时先做一致性快照,之后持续捕获已提交的INSERTUPDATEDELETE事件,并把变更事件写入Kafka topic。

典型链路:

sequenceDiagram
    participant App as 业务服务
    participant DB as PostgreSQL
    participant WAL as WAL
    participant D as Debezium
    participant K as Kafka
    participant C as Cache Consumer
    participant R as Redis

    App->>DB: INSERT / UPDATE / DELETE
    DB->>WAL: commit后写入WAL
    D->>WAL: logical decoding
    D->>K: change event
    C->>K: consume event
    C->>R: DEL / SET / HSET

PostgreSQL侧常见前置条件:

1
wal_level = logical

复制用户需要具备复制权限:

1
CREATE ROLE debezium_user REPLICATION LOGIN;

Debezium connector配置形态大致如下,真实环境不要把密码写进配置仓库:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "app-postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium_user",
"database.dbname": "app",
"topic.prefix": "app",
"table.include.list": "public.users,public.orders"
}
}

缓存消费者根据事件操作Redis:

DB事件 Redis处理 说明
INSERT SET / HSET / DEL 可以直接写缓存,也可以只删除相关缓存
UPDATE 优先DEL 避免字段计算不一致,下一次读再重建
DELETE DEL 删除对象缓存和相关索引缓存

CDC同步Redis的优势

  • 业务代码不必到处写缓存删除逻辑;
  • DB提交成功后,变更事件由日志驱动,不依赖业务服务临时状态;
  • 多个下游可以共享同一份变更流,比如Redis、搜索索引、审计系统;
  • 可以把缓存同步从请求链路中移出,降低业务接口延迟。

CDC不是强一致魔法

CDC解决的是“变更最终能被看见”,不等于“Redis和DB每一毫秒都一致”。

风险点:

  • CDC有延迟,Redis短时间可能还是旧值;
  • Kafka或消费者积压会扩大不一致窗口;
  • replication slot如果长期不消费,PostgreSQL WAL可能堆积;
  • 消费者必须幂等,重复事件不能写坏缓存;
  • 表结构变化会影响事件格式;
  • 初始快照和增量切换期间要规划好缓存预热和读策略。

一个务实原则:CDC适合做最终一致性兜底和大规模解耦,不适合替代关键请求里的强一致校验

5. 性能调优、实战避坑与一致性保障

5.1 缓存与数据库一致性:核心难点

缓存一致性不是一道单选题。它的难点来自三件事:

  • DB和Redis是两个系统,无法天然共享一个本地事务;
  • 请求是并发的,读写顺序可能交错;
  • 网络、进程、MQ、CDC都有延迟和失败。

所以工程目标通常不是“永远强一致”,而是先分级:

数据类型 一致性目标 典型处理
余额、库存、权限判定 强一致或接近强一致 DB事务、唯一约束、锁、版本号
用户资料、商品详情 短暂最终一致 Cache Aside、删除缓存、MQ重试
排行榜、计数、热度 最终一致 异步汇总、定期校正
推荐、浏览量、UV 可容忍误差 批量同步、降级、离线修正

5.2 经典缓存更新模式

Cache Aside Pattern:旁路缓存

这是最常见、最实用的模式。应用程序同时访问缓存和数据库。

读流程:

flowchart TD
    A["读请求"] --> B{"Redis命中?"}
    B -- "yes" --> C["返回缓存"]
    B -- "no" --> D["查询DB"]
    D --> E["写入Redis并设置TTL"]
    E --> F["返回结果"]

写流程:

1
2
3
更新DB
-> 删除缓存
-> 下一次读请求重新加载缓存

优点:

  • 简单,应用可控;
  • Redis故障时可降级读DB;
  • 不强迫所有数据都进缓存;
  • 适合大多数后端服务。

缺点:

  • 缓存逻辑在业务代码里;
  • 并发读写下仍有短暂不一致;
  • 需要处理缓存穿透、击穿、雪崩。

Read Through / Write Through

Read Through和Write Through里,应用程序理论上只和缓存层交互,由缓存代理负责读写DB。

1
2
3
4
应用
-> 缓存代理
-> Redis
-> DB

优点:

  • 应用代码更干净;
  • 缓存和DB读写策略集中管理。

缺点:

  • 需要专门缓存代理或框架支持;
  • 代理成为复杂组件;
  • Redis本身不是关系型DB代理,很多时候需要自己实现。

适合:统一缓存平台、规范化强的业务中台、读写路径可控的系统。

Write Behind:异步缓存写入

Write Behind也叫Write Back。应用先写缓存,缓存层再异步写DB。

1
2
3
4
应用写入
-> Redis
-> 异步队列
-> DB

优点:

  • 写入延迟极低;
  • 能批量落库,提高吞吐。

缺点:

  • Redis或异步队列故障可能导致数据丢失;
  • 数据恢复和重放复杂;
  • DB不再是实时事实源;
  • 对审计、事务和幂等要求很高。

适合:日志、计数、行为轨迹这类可补偿、可重放、可接受延迟的数据。不适合订单支付、库存扣减、账户余额等核心事实。

5.3 淘汰缓存 vs 更新缓存

写DB后,缓存到底应该“更新”还是“删除”?

很多大型团队更偏向删除缓存,而不是直接更新缓存。

原因不是“更新缓存做不到”,而是并发场景里更新缓存很容易发生时序错乱。

假设两个请求同时更新同一个商品:

sequenceDiagram
    participant A as 请求A
    participant B as 请求B
    participant DB as DB
    participant R as Redis

    A->>DB: 更新价格=100
    B->>DB: 更新价格=120
    B->>R: SET price=120
    A->>R: SET price=100

最终DB里是120,Redis里却可能是旧值100

如果改成删除缓存:

1
2
3
更新DB
-> DEL cache
-> 下一次读DB重建缓存

删除的好处是把复杂的“计算新缓存值”推迟到读路径,并让缓存从DB这个事实源重新构建。即使多删几次,最多导致缓存未命中;而乱更新一次,可能留下长期脏数据。

5.4 先删缓存还是先更新DB

方案一:先删缓存,再更新DB

1
2
DEL cache
UPDATE db

并发问题:

sequenceDiagram
    participant A as 写请求A
    participant B as 读请求B
    participant DB as DB
    participant R as Redis

    A->>R: DEL cache
    B->>R: GET cache miss
    B->>DB: SELECT old value
    B->>R: SET old value
    A->>DB: UPDATE new value

最终DB是新值,但Redis可能被读请求写回旧值。

方案二:先更新DB,再删缓存

1
2
UPDATE db
DEL cache

这是Cache Aside里更常见的基线方案。

它也不是绝对无风险。极端并发下可能出现:

sequenceDiagram
    participant B as 读请求B
    participant A as 写请求A
    participant DB as DB
    participant R as Redis

    B->>R: GET cache miss
    B->>DB: SELECT old value
    A->>DB: UPDATE new value
    A->>R: DEL cache
    B->>R: SET old value

这个窗口通常比“先删缓存再更新DB”小,因为读请求必须刚好在缓存未命中、读到旧DB值、并且写缓存晚于删除操作。但它仍然存在。

实践建议:

  • 默认采用“更新DB后删除缓存”;
  • 删除缓存失败要有重试;
  • 对热点Key可配合互斥锁或逻辑过期;
  • 对关键数据不要只靠缓存一致性,DB约束和事务仍是事实源。

5.5 延迟双删策略

用户给出的经典流程是:

1
2
3
4
先删缓存
-> 更新DB
-> 休眠N毫秒
-> 再次删除缓存

它的目的,是清理并发读请求在第一次删除后、DB更新前写回的旧缓存。

sequenceDiagram
    participant A as 写请求A
    participant B as 读请求B
    participant DB as DB
    participant R as Redis

    A->>R: DEL cache
    B->>R: GET miss
    B->>DB: SELECT old value
    B->>R: SET old value
    A->>DB: UPDATE new value
    A->>A: sleep N ms
    A->>R: DEL cache again

N怎么估算?

1
N > 读请求从DB读旧值并写回Redis的最长常见耗时

可以参考:

  • DB查询P99耗时;
  • Redis写入P99耗时;
  • 服务线程调度和网络抖动;
  • 主从复制延迟;
  • 业务请求超时时间。

如果读DB P99是80ms,Redis写入P99是10ms,请求链路偶发调度20ms,那么N可以先从100ms ~ 300ms级别试验,而不是拍脑袋写固定1秒。关键是结合监控看效果。

注意:

  • 延迟双删不是强一致方案;
  • 在请求线程里sleep会占用资源,生产中更适合投递延迟任务;
  • N太短没效果,太长会延长不一致窗口或增加无意义删除;
  • 如果缓存重建非常频繁,双删会放大缓存未命中压力。

5.6 强一致性方案:锁与串行化

如果业务确实要求强一致,可以用锁把同一个资源的读写串行化。

1
2
3
4
获取锁
-> 读/写DB
-> 更新或删除Redis
-> 释放锁

Java生态里常见用Redisson实现Redis分布式锁或读写锁。它能减少自己手写SET NX PX和Lua释放锁的细节风险。

但锁不是免费方案:

  • 会降低并发;
  • 锁粒度设计不好会造成排队;
  • 锁超时时间过短会误释放,过长会拖慢恢复;
  • 分布式锁不能替代DB唯一约束、事务和状态机;
  • Redis故障时,锁服务本身也会影响业务。

适合加锁的场景:

  • 同一商品库存扣减;
  • 同一订单状态流转;
  • 同一用户关键额度变更;
  • 热点Key缓存重建。

不适合:

  • 所有读请求都加锁;
  • 对全局资源加大锁;
  • 用锁掩盖不清晰的状态机设计。

5.7 最终一致性兜底:MQ与CDC

缓存删除失败是常见故障:

1
2
UPDATE db success
DEL cache failed

如果没有兜底,旧缓存会一直存在到TTL过期。

MQ重试

可以把缓存删除消息投递到MQ:

flowchart TD
    A["更新DB成功"] --> B["发送缓存删除消息"]
    B --> C["MQ"]
    C --> D["消费者DEL cache"]
    D --> E{"成功?"}
    E -- "yes" --> F["结束"]
    E -- "no" --> G["重试 / 死信队列 / 告警"]

更稳的做法是Outbox模式:

1
2
3
4
5
6
7
8
DB事务中:
-> 更新业务表
-> 写outbox事件表

事务后:
-> 后台任务扫描outbox
-> 投递MQ
-> 消费者删除缓存

这样可以避免“DB更新成功,但消息还没发出去服务就崩了”的问题。

CDC兜底

CDC方案可以从DB日志里捕获变更:

1
2
3
4
5
6
DB commit
-> WAL / Binlog
-> Debezium
-> Kafka
-> Cache Consumer
-> DEL Redis key

它适合作为最终一致性兜底:

  • 业务代码漏删缓存,CDC还能捕获DB变更;
  • 多服务共享同一张表时,不必每个服务都写缓存同步逻辑;
  • 可以统一做缓存删除、搜索索引更新、审计事件。

但CDC也要监控延迟。如果消费者积压,Redis可能长时间落后于DB。

5.8 缓存穿透

缓存穿透指查询一个不存在的数据,缓存没有,DB也没有。如果恶意请求大量不存在ID,会绕过缓存直接打DB。

1
2
3
4
GET cache miss
-> SELECT db miss
-> 下一次同样miss
-> DB持续承压

对策:

方案 思路 注意事项
缓存空值 DB查不到时缓存null短TTL 防止空值过多,TTL要短
布隆过滤器 先判断ID是否可能存在 有误判,无漏判;需要维护过滤器
参数校验 非法ID直接拒绝 对恶意请求很有效
限流 对异常来源限速 防止被刷穿

布隆过滤器适合“存在集合相对稳定”的场景,比如商品ID、用户ID。如果数据频繁新增,要处理过滤器更新;如果需要删除,普通布隆过滤器不够,需要计数布隆过滤器或重建。

5.9 缓存击穿

缓存击穿指一个热点Key突然过期,大量请求同时发现缓存未命中,然后一起打到DB。

flowchart TD
    A["热点Key过期"] --> B["大量请求miss"]
    B --> C["同时查询DB"]
    C --> D["DB压力飙升"]

对策:

互斥锁

只有一个请求重建缓存,其他请求等待或返回旧值。

1
2
3
4
GET cache miss
-> SET lock NX PX
-> 成功:查询DB并重建缓存
-> 失败:短暂等待后重试 / 返回旧缓存

逻辑过期

缓存值里存一个逻辑过期时间,不让Redis自动删除热点Key。

1
2
3
4
5
6
7
{
"data": {
"id": 1001,
"name": "item"
},
"expire_at": "2026-06-03T10:00:00+08:00"
}

读到逻辑过期数据时:

  • 先返回旧值,保证请求不打爆DB;
  • 后台异步重建缓存;
  • 适合热点商品、首页配置等允许短暂旧值的场景。

5.10 缓存雪崩

缓存雪崩指大量Key同时过期,或者Redis整体不可用,导致请求集中打到DB。

对策:

风险 对策
大量Key同一时间过期 TTL加随机抖动
Redis节点故障 主从、Sentinel、Cluster、多级缓存
DB承接不了回源流量 限流、熔断、降级
冷启动缓存为空 缓存预热
热点缓存过期 逻辑过期、互斥重建

TTL随机抖动示例:

1
2
3
base_ttl = 3600s
random_jitter = 0~300s
final_ttl = base_ttl + random_jitter

雪崩治理的重点是“分散风险”,不要让大量请求在同一时刻做同一件昂贵的事。

5.11 Big Key排查与处理

Big Key是指value很大,或者集合成员很多的key。

典型例子:

1
2
3
4
5
String: 单个value几十MB
Hash: 一个key下几十万field
List: 一个队列堆积千万元素
Set: 一个标签集合包含海量用户
ZSet: 排行榜无限增长

危害:

  • 读写网络传输大;
  • 删除时可能阻塞;
  • RDB/AOF和复制压力变大;
  • Cluster迁移槽位时耗时长;
  • 客户端容易超时。

常用排查:

1
redis-cli --bigkeys

查看单个key内存:

1
2
3
4
5
6
MEMORY USAGE user:1001
TYPE user:1001
HLEN user:1001
LLEN queue:jobs
SCARD tag:backend
ZCARD rank:daily

渐进式扫描:

1
SCAN 0 MATCH user:* COUNT 100

处理方式:

  • 大对象拆分成多个小key;
  • 集合分页,不要一次取全量;
  • List/ZSet加长度上限;
  • 删除大Key优先使用UNLINK,避免同步删除阻塞;
  • 对过期大Key做分批清理;
  • 设计阶段就限制单key最大体积。

5.12 Hot Key排查与处理

Hot Key是访问量极高的key。它可能不大,但会让某个Redis节点CPU、网络或连接数飙升。

常见来源:

  • 首页配置;
  • 秒杀商品;
  • 热门排行榜;
  • 用户全局权限;
  • 单个设备或航线状态被高频读取;
  • 锁Key竞争激烈。

排查方法:

1
redis-cli --hotkeys

注意:--hotkeys依赖Redis的LFU访问频率统计,实践中要确认当前版本和淘汰策略是否支持观察到有效频次。

也可以结合:

1
2
3
4
INFO commandstats
INFO stats
LATENCY DOCTOR
SLOWLOG GET 20

处理方式:

  • 本地缓存:应用内短TTL缓存热点配置;
  • 多副本读:热点读分散到副本;
  • Key分片:例如计数器拆成counter:{id}:0..N
  • 提前预热:活动开始前加载热点数据;
  • 逻辑过期:热点Key不过期,由后台刷新;
  • 限流降级:极端流量下返回降级结果。

5.13 Slowlog慢查询日志

Redis Slowlog记录执行时间超过阈值的命令。它记录的是命令执行耗时,不包含网络传输和排队时间。

常用配置:

1
2
slowlog-log-slower-than 10000
slowlog-max-len 128

slowlog-log-slower-than 10000表示超过10000微秒,也就是10ms的命令会被记录。

查看慢查询:

1
2
3
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET

慢查询常见原因:

  • 对大集合执行全量命令;
  • Lua脚本执行过久;
  • 删除大Key;
  • 排序、交集、范围查询返回过多数据;
  • 网络慢不一定进Slowlog,因为Slowlog不记录网络时间。

排查时不要只看Slowlog,也要结合:

1
2
3
4
INFO stats
INFO commandstats
INFO memory
LATENCY DOCTOR

5.14 一套生产落地清单

上线Redis缓存或高可用架构前,可以按这张清单过一遍。

项目 检查点
容量 是否设置maxmemory,是否预估对象大小和Key数量
淘汰 maxmemory-policy是否符合数据类型
TTL 缓存Key是否都有TTL,TTL是否有随机抖动
持久化 RDB/AOF/混合持久化是否符合丢失窗口
高可用 单实例、Sentinel、Cluster选择是否匹配业务规模
一致性 是否明确Cache Aside、MQ重试或CDC兜底
热点 是否识别热点Key,是否有本地缓存或逻辑过期
大Key 是否限制集合大小,是否有Big Key巡检
慢查询 是否配置Slowlog,是否接入监控
降级 Redis不可用时业务如何处理
安全 是否限制网络暴露、认证、ACL和密钥管理

6. 面试与工程表达

如果面试或方案评审问“Redis高可用和一致性怎么设计”,不要只背概念,可以按下面结构回答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. Redis定位
-> 缓存 / 计数 / 排行榜 / 锁,不作为核心事实源

2. 持久化
-> RDB快照 + AOF everysec / 混合持久化
-> 说明可接受丢失窗口

3. 高可用
-> 主从复制解决副本
-> Sentinel解决自动故障转移
-> Cluster解决容量和写入分片

4. 一致性
-> Cache Aside
-> 更新DB后删除缓存
-> 删除失败用MQ重试或CDC兜底
-> 强一致场景用DB事务、约束、锁

5. 风险治理
-> 穿透 / 击穿 / 雪崩
-> Big Key / Hot Key
-> Slowlog / INFO / LATENCY

一个成熟回答的重点是:你能说清楚Redis解决了什么,也能说清楚它解决不了什么。

7. 参考资料


Redis原理和高可用
https://www.albeltbo.com/posts/ee4904fc.html
作者
AlbeltBo
发布于
2026年7月14日
许可协议