缓存01:Redis
从数据结构、持久化策略、集群模式、哨兵机制到缓存设计模式,拆解 Redis 在分布式缓存场景下的核心机制和常见坑点。
Redis 不是”简单缓存”,而是”数据结构服务器”。它支持五种基础数据结构(String、List、Set、Hash、ZSet)和四种高级结构(Bitmap、HyperLogLog、Geo、Stream),能够承载计数器、排行榜、消息队列、分布式锁、位运算等复杂场景。
这篇文章围绕五个核心机制:数据结构、持久化、集群模式、哨兵机制、缓存设计模式,把 Redis 的设计逻辑和常见坑点讲清楚。
为什么选 Redis,而不是别的
先定位清楚:Redis 的核心场景是高性能缓存和复杂数据结构操作,支持内存级读写、多种数据结构和持久化机制,适合计数器、排行榜、分布式锁、会话存储、消息队列等场景。
与 Memcached、Caffeine(本地缓存)的差异:
| 维度 | Redis | Memcached | Caffeine(本地缓存) |
|---|---|---|---|
| 数据结构 | 5 种基础 + 4 种高级 | 只支持 String | 只支持 Key-Value |
| 持久化 | RDB + AOF,支持数据恢复 | 不支持,重启丢数据 | 不支持,进程重启丢数据 |
| 集群 | 主从、哨兵、Cluster 三种模式 | 不原生支持集群 | 不支持,单机 |
| 分布式 | 支持分布式锁、分布式计数 | 不支持 | 不支持,只单机 |
| 吞吐量 | 单机十万级 QPS | 单机十万级 QPS | 单机百万级 QPS(内存访问) |
| 适用场景 | 分布式缓存、计数器、排行榜、分布式锁、消息队列 | 简单 KV 缓存、Session 共享 | 本地热点缓存、高频读 |
简单判断:分布式场景、需要复杂数据结构、需要持久化 → Redis 合适。简单 KV 缓存、跨语言 Session → Memcached 合适。单机热点数据、高频读 → Caffeine 合适。
数据结构:五种基础 + 四种高级
Redis 的核心优势是丰富的数据结构,不同结构对应不同场景。
五种基础数据结构
| 数据结构 | 说明 | 适用场景 |
|---|---|---|
| String | 最基础类型,支持数值操作(INCR/DECR) | 计数器、分布式锁、缓存对象(JSON 序列化) |
| List | 双向链表,支持 LPUSH/RPUSH/LPOP/RPOP | 消息队列、最新列表、时间线 |
| Set | 无序集合,支持交集、并集、差集 | 标签系统、共同好友、去重 |
| Hash | 键值对集合,适合存储对象 | 用户信息、商品属性、计数器(多字段) |
| ZSet(Sorted Set) | 有序集合,按分数排序 | 排行榜、延时队列、按时间排序的列表 |
String:不只是字符串
String 是 Redis 最基础的数据结构,但它的能力不止存储字符串:
- 数值操作:
INCR key、INCRBY key 10、DECR key,适合计数器(如文章阅读数、点赞数) - 分布式锁:
SET key value NX PX 30000,NX 保证不存在才设置,PX 设置过期时间 - 缓存对象:对象序列化为 JSON/String 存储,适合缓存用户信息、商品详情
常见坑:大 Value 问题。String 存储的对象过大(超过 10KB),读写性能下降,建议拆成 Hash 多字段存储。
List:消息队列的替代
List 是双向链表,支持 LPUSH/RPUSH(头尾插入)和 LPOP/RPOP(头尾弹出):
- LPUSH + RPOP:生产者从左侧插入,消费者从右侧弹出,先进先出(FIFO)
- RPUSH + LPOP:生产者从右侧插入,消费者从左侧弹出,先进先出(FIFO)
- BRPOP key timeout:阻塞弹出,timeout 秒内没有元素则阻塞等待,类似长轮询
适用场景:简单消息队列、最新文章列表、用户时间线。
常见坑:List 不支持消费确认和重试,消息弹出后立即删除,消费失败无法恢复。适合轻量队列,不适合业务消息。业务消息用 Kafka/RocketMQ/RabbitMQ。
Set:去重和集合运算
Set 是无序集合,元素唯一,支持交集、并集、差集:
SADD key member:添加元素SINTER key1 key2:交集,适合共同好友、共同标签SUNION key1 key2:并集,适合合并标签、合并关注列表SDIFF key1 key2:差集,适合差集计算
适用场景:标签系统、共同好友、去重、社交关系。
Hash:对象的天然存储
Hash 是键值对集合,适合存储对象的多个字段:
HSET key field value:设置字段值HGET key field:获取字段值HMGET key field1 field2:批量获取字段HINCRBY key field increment:字段数值递增,适合多字段计数器
适用场景:用户信息(userId、name、age、email)、商品属性(productId、price、stock、title)、多字段计数器(文章的阅读数、点赞数、评论数)。
Hash 与 String 存储对象的对比:
| 维度 | String(JSON 序列化) | Hash(多字段) |
|---|---|---|
| 读写方式 | 整体读写,修改需全量更新 | 单字段读写,修改只更新字段 |
| 适用场景 | 整体缓存、对象完整读取 | 对象部分字段、频繁修改部分字段 |
| 内存占用 | 整体占用,较大 | 按字段占用,灵活 |
ZSet:排行榜的天然实现
ZSet 是有序集合,每个元素关联一个分数(score),按分数排序:
ZADD key score member:添加元素和分数ZRANGE key start stop WITHSCORES:按分数升序获取ZREVRANGE key start stop WITHSCORES:按分数降序获取,适合排行榜ZINCRBY key increment member:分数递增,适合动态排名
适用场景:排行榜(文章热度、用户积分)、延时队列(score = 延迟时间戳)、按时间排序的列表(score = 时间戳)。
四种高级数据结构
| 高级结构 | 说明 | 适用场景 |
|---|---|---|
| Bitmap | 位图,按位操作(SETBIT/GETBIT) | 用户签到、用户活跃度统计、布隆过滤器 |
| HyperLogLog | 基数估算,误差 0.81% | UV 统计、去重计数(不精确) |
| Geo | 地理坐标,支持距离计算、范围查询 | 附近的人、附近的门店、地理范围查询 |
| Stream | 消息流,支持消费组、消息确认 | 消息队列(Redis 5.0+)、日志流 |
持久化:RDB 和 AOF
Redis 的持久化决定数据是否能在重启后恢复。两种持久化方式各有取舍。
RDB(Redis Database)
RDB 是快照持久化,定期将内存数据保存到磁盘的二进制文件(dump.rdb):
- 触发方式:配置规则(如 900 秒内至少 1 次修改触发)或手动执行
BGSAVE - 写入方式: fork 子进程,子进程将内存数据写入临时文件,完成后替换旧 RDB 文件
- 恢复速度:快,直接加载 RDB 文件到内存
- 数据完整性:差,最后一次快照后的修改会丢失
RDB 配置:
save 900 1 # 900 秒内至少 1 次修改触发
save 300 10 # 300 秒内至少 10 次修改触发
save 60 10000 # 60 秒内至少 10000 次修改触发
适用场景:允许分钟级数据丢失、恢复速度优先、数据量大的场景。
AOF(Append Only File)
AOF 是日志持久化,每次写入操作追加到 AOF 文件:
- 写入方式:每次写入操作追加到 AOF 文件(appendonly.aof)
- 刷盘策略:
appendfsync always(每次写入刷盘)、appendfsync everysec(每秒刷盘)、appendfsync no(由操作系统决定) - 恢复速度:慢,需要重放所有写入操作
- 数据完整性:好,最多丢失 1 秒数据(everysec 策略)
AOF 配置:
appendonly yes
appendfsync everysec # 每秒刷盘,推荐
AOF 重写:AOF 文件会越来越大,Redis 定期重写 AOF,压缩冗余命令(如多次 INCR 合并为一条 SET)。触发规则:auto-aof-rewrite-min-size 64mb(文件超过 64MB 触发重写)。
适用场景:数据完整性优先、允许秒级丢失、恢复速度可接受的场景。
RDB + AOF 混合持久化
Redis 4.0+ 支持混合持久化:RDB 做基础快照,AOF 记录增量操作。恢复时先加载 RDB,再重放 AOF 增量。
配置:
aof-use-rdb-preamble yes
适用场景:兼顾恢复速度和数据完整性,推荐生产环境使用。
持久化对比
| 维度 | RDB | AOF | RDB + AOF 混合 |
|---|---|---|---|
| 恢复速度 | 快 | 慢 | 中等 |
| 数据完整性 | 差(分钟级丢失) | 好(秒级丢失) | 好(秒级丢失) |
| 文件大小 | 小(压缩快照) | 大(日志追加) | 中等 |
| 性能影响 | 低(定期快照) | 中(每次写入追加) | 中 |
| 适用场景 | 允许分钟级丢失、恢复快 | 数据完整性优先 | 兼顾两者,推荐生产 |
集群模式:主从、哨兵、Cluster
Redis 的集群模式决定高可用和水平扩展能力。三种模式各有取舍。
主从复制(Master-Slave)
主从复制是最基础的高可用方案:
- Master:读写请求,数据写入 Master
- Slave:只读请求,从 Master 同步数据
配置:Slave 执行 REPLICAOF master_ip master_port,建立复制关系。
主从复制的限制:
- Master 故障需人工切换:Slave 只读,Master 故障后需手动提升 Slave 为 Master
- 写入压力集中在 Master:Slave 只分担读压力,写入压力仍在 Master
- 复制延迟:Slave 同步有延迟,Slave 读到的是旧数据
适用场景:读多写少、允许短暂读延迟、人工切换可接受的场景。
哨兵模式(Sentinel)
哨兵模式在主从基础上增加自动故障转移:
- Sentinel:监控 Master 和 Slave,检测 Master 故障后自动切换
- 选举机制:Sentinel 集群投票选举新 Master,从 Slave 中选择
- 客户端感知:Sentinel 通知客户端新 Master 地址,客户端自动切换
配置:启动 Sentinel 进程,配置监控的 Master 地址和 Sentinel 集群。
哨兵模式的限制:
- 写入压力仍在 Master:Slave 只读,哨兵解决故障转移,不解决写入压力
- Slave 数量受限:Slave 过多时同步延迟增加
- 选举延迟:Sentinel 检测故障和选举需要时间(秒级)
适用场景:读多写少、需要自动故障转移、写入压力不大的场景。
Cluster 模式
Cluster 模式是 Redis 的分布式方案,支持水平扩展:
- 分片:数据按 Key 分片到多个 Master 节点,每个 Master 负责 16384 个槽位的一部分
- 多 Master:多个 Master 同时写入,写入压力分散
- 高可用:每个 Master 配备 Slave,Master 故障时 Slave 自动提升
分片规则:CRC16(key) % 16384,根据槽位路由到对应 Master。
Cluster 模式的限制:
- 跨节点操作不支持:MSET、事务(MULTI/EXEC)跨节点不支持
- Lua 脚本受限:Lua 脚本只能在单节点执行
- Key 设计重要:相关 Key 需要路由到同一节点,使用 Hash Tag(如
{user:1001}:profile、{user:1001}:score)
适用场景:写入压力大、需要水平扩展、数据量大的场景。
集群模式对比
| 维度 | 主从复制 | 哨兵模式 | Cluster 模式 |
|---|---|---|---|
| 高可用 | 需人工切换 | 自动故障转移 | 自动故障转移 |
| 写入扩展 | 不支持 | 不支持 | 支持多 Master 写入 |
| 读扩展 | Slave 分担读压力 | Slave 分担读压力 | Slave 分担读压力 |
| 跨节点操作 | 支持(单节点) | 支持(单节点) | 不支持(需 Hash Tag) |
| 适用场景 | 读多写少、人工切换可接受 | 读多写少、自动故障转移 | 写入压力大、水平扩展 |
缓存设计模式:三种常见模式
Redis 作为缓存时,有三种常见设计模式,各有取舍。
Cache-Aside Pattern(旁路缓存)
最常用的缓存模式:应用代码同时维护缓存和数据库:
- 读流程:先读缓存,命中则返回;未命中则读数据库,写入缓存后返回
- 写流程:先更新数据库,再删除缓存(不是更新缓存)
为什么要删除缓存而不是更新缓存:
| 操作 | 说明 |
|---|---|
| 更新缓存 | 每次写数据库都更新缓存,并发写时缓存数据不一致 |
| 删除缓存 | 删除后下次读时重新加载,保证一致性 |
Cache-Aside 的常见坑:并发读写导致脏数据。线程 A 读数据库(旧数据),线程 B 更新数据库并删除缓存,线程 A 写入缓存(旧数据),缓存数据不一致。
解决方式:延迟双删,更新数据库后删除缓存,延迟一段时间(如 500ms)再次删除缓存。
Read-Through / Write-Through Pattern(穿透缓存)
缓存代理数据库访问:应用代码只访问缓存,缓存未命中时由缓存组件读数据库并写入缓存。
- Read-Through:应用读缓存,缓存组件负责读数据库并写入缓存
- Write-Through:应用写缓存,缓存组件负责同步写数据库
优点:应用代码不关心数据库,缓存组件统一维护一致性。缺点:缓存组件复杂,需要实现数据库访问逻辑。
适用场景:缓存组件成熟、应用代码简化、一致性统一维护的场景。
Write-Behind Pattern(异步写缓存)
写缓存时异步写数据库:应用写缓存后立即返回,缓存组件异步批量写数据库。
优点:写入性能高,应用不需要等待数据库写入。缺点:数据库写入延迟,可能丢数据(缓存组件故障)。
适用场景:写入性能优先、允许数据库写入延迟、数据丢失可接受的场景。
三种模式对比
| 模式 | 读流程 | 写流程 | 一致性 | 性能 |
|---|---|---|---|---|
| Cache-Aside | 先读缓存,未命中读 DB 写缓存 | 先写 DB,再删缓存 | 中等(并发写可能不一致) | 高 |
| Read-Through | 读缓存,缓存组件读 DB 写缓存 | 写缓存,缓存组件同步写 DB | 好(缓存组件统一维护) | 中等 |
| Write-Behind | 读缓存,缓存组件读 DB 写缓存 | 写缓存,缓存组件异步写 DB | 差(异步写入延迟) | 最高 |
生产环境常用 Cache-Aside,简单可控。Read-Through/Write-Through 需成熟缓存组件(如 Spring Cache + Redis),Write-Behind 适合高性能写入场景(如埋点数据)。
常见坑点
| 坑点 | 说明 | 解决方式 |
|---|---|---|
| 缓存穿透 | Key 不存在,请求穿透到数据库 | 布隆过滤器拦截不存在 Key、空值缓存短 TTL |
| 缓存击穿 | 热点 Key 过期瞬间大量请求穿透到数据库 | 永不过期、互斥锁(SETNX)、逻辑过期 |
| 缓存雪崩 | 大量 Key 同时过期,数据库压力瞬间激增 | 过期时间随机化、多级缓存、熔断限流 |
| 大 Key 问题 | 单个 Key 的 Value 过大(>10KB) | 拆分 Key、使用 Hash 多字段存储 |
| 热点 Key 问题 | 单个 Key 访问量过高,单节点压力过大 | 本地缓存(Caffeine)+ Redis、Key 分片 |
| 缓存与数据库不一致 | 并发读写、先删缓存再写 DB 导致不一致 | 先写 DB 再删缓存、延迟双删、分布式锁 |
项目判断
引入 Redis 时,先回答三个问题:
- 场景:是简单缓存,还是需要复杂数据结构(排行榜、分布式锁、计数器)?
- 数据量:数据量是否超过单机内存(需要 Cluster)?
- 一致性要求:是否需要强一致性(缓存不适合,用数据库)?
如果是简单缓存、数据量不大、一致性要求不高 → 单机 Redis 或主从即可。如果需要复杂数据结构、高可用 → 哨兵模式。如果数据量大、写入压力大 → Cluster 模式。
线上排查 Redis 问题:
- 慢查询:
SLOWLOG GET 10查看慢命令,检查是否大 Key、复杂命令(如 KEYS *) - 内存使用:
INFO memory查看内存占用,检查是否有大 Key、未设置过期时间 - 阻塞命令:避免 KEYS *、HGETALL 大 Hash、ZRANGE 大 ZSet,用 SCAN 替代
- 连接数:
INFO clients查看连接数,检查是否有连接泄漏 - 持久化:检查 RDB/AOF 配置,是否开启持久化,刷盘策略是否合理