Redis 实战:缓存策略、数据结构与性能调优
缓存策略
最常见的缓存策略是 Cache-Aside Pattern:
- 读请求先查缓存,命中则返回
- 未命中则查数据库,写入缓存,返回
- 写请求先更新数据库,再删除缓存
注意是删除缓存而不是更新缓存。删除是幂等的,而更新可能产生数据不一致。
为什么是“先更新数据库,再删缓存”
先删缓存再更新数据库存在明显竞态:删除后、数据库更新完成前,另一个读请求未命中缓存,把旧数据写入缓存,此后一直是脏数据。而“先更新数据库,再删缓存”的窗口期极短(要求读请求恰好读到旧库数据、又恰好没命中旧缓存),概率低得多,但没有绝对安全的方案。
更进一步,如果删除缓存这一步失败,数据依然不一致。可以引入消息队列做删除重试,或使用 Canal 订阅 binlog 异步删除,把“最终一致”做扎实。
缓存问题与解决方案
- 缓存穿透:查询不存在的数据,可使用布隆过滤器或空值缓存
- 缓存击穿:热点 key 过期,可使用互斥锁或永不过期策略
- 缓存雪崩:大量 key 同时过期,可在过期时间上增加随机值
三种问题容易混淆,一个记忆方法:穿透是“查不存在的数据”(请求穿过了缓存和数据库两层防线),击穿是“单个热点 key 失效”(被高并发流量击穿一点),雪崩是“大面积同时失效”(整体崩塌)。
# 空值缓存防穿透:短 TTL 防止被恶意 key 撑爆内存
SET user:404 "" EX 30
# 互斥锁防击穿:拿不到锁的请求短暂等待后重试
SET lock:hotkey 1 NX EX 3
数据结构选择
Redis 有丰富的数据结构,选对结构能极大提升效率。
ZADD leaderboard 100 "user1"
ZADD leaderboard 200 "user2"
ZRANGE leaderboard 0 9 REV
INCR page:views:home
LPUSH queue "task1"
BRPOP queue 0
各数据结构的典型场景
- String:缓存对象 JSON、计数器(INCR/INCRBY 原子自增)、分布式锁
- Hash:对象的多字段读写,可以只更新单个字段而不用序列化整个对象
- List:消息队列、最新动态列表,LPUSH + BRPOP 实现简单阻塞队列
- Set:去重、共同关注(SINTER 交集)、抽奖(SPOP 随机弹出)
- ZSet:排行榜、延迟队列(score 存执行时间戳,定时 ZRANGEBYSCORE 捞任务)
延迟队列的实现示例
# 订单 30 分钟未支付自动取消:score 为到期时间戳
ZADD order:delay 1735689600 "order:1001"
ZADD order:delay 1735691400 "order:1002"
# 消费者每秒轮询,只取已到期的任务
ZRANGEBYSCORE order:delay 0 1735689700 LIMIT 0 10
# 处理成功后移除
ZREM order:delay "order:1001"
一个容易忽视的坑:bigkey
String 超过 10KB、集合类型元素超过 5000 个就算 bigkey。危害包括:
- 操作耗时上升,阻塞单线程的 Redis
- 集群模式下数据倾斜
- 删除大 key 时(尤其 4.0 之前)同步阻塞主线程
# 线上排查 bigkey,只扫描从库
redis-cli --bigkeys -i 0.1
# 4.0+ 用 UNLINK 异步删除替代 DEL
UNLINK huge:list:key
分布式锁的正确姿势
Redis 实现分布式锁看似简单,细节却很多:
# 加锁:NX 保证互斥,EX 设置过期防止死锁
SET lock:order:1001 "unique-token-abc" NX EX 10
两个关键细节:
- value 必须是唯一标识(如 UUID):防止 A 的锁超时自动释放后,B 拿到锁,A 执行 DEL 时误删 B 的锁。删除前要用 Lua 脚本校验 value
- 锁的续期:业务执行超过锁的 TTL 时,需要看门狗机制自动续期,这正是 Redisson 的 watchDog 做的事
-- 释放锁:校验持有者再删除,必须原子执行
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
如果业务对锁的正确性要求极高(丢失锁会造成资损),考虑 RedLock 或改用 etcd/ZooKeeper 这类强一致组件,Redis 单实例锁在主从切换时可能同时被两个客户端持有。
持久化与高可用
- RDB:定时快照,文件小恢复快,但会丢最后一次快照之后的数据。适合容忍分钟级丢失的场景
- AOF:追加写命令,
appendfsync everysec是常见的折中配置,最多丢 1 秒数据 - 混合持久化(4.0+):RDB 做全量 + AOF 做增量,兼顾恢复速度和数据安全,推荐开启
# redis.conf 关键配置
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
高可用部署上,哨兵(Sentinel) 负责主从自动故障转移,Cluster 则解决容量和写性能的水平扩展(16384 个 slot 分布到多节点)。集群模式下注意:多 key 操作(MGET、事务、Lua)要求 key 落在同一个 slot,需要用 hash tag {user:1001}:profile 强制路由。
性能调优清单
线上 Redis 变慢时按这个顺序排查:
- 慢查询日志:
SLOWLOG GET 10找出耗时命令,优先处理 KEYS、SMEMBERS 这类 O(N) 命令,禁用 KEYS * - 持久化阻塞:AOF 的 fsync、RDB 的 fork 都可能阻塞主线程,关注
latest_fork_usec指标 - 内存与淘汰:
maxmemory配合allkeys-lru淘汰策略,避免用默认的noeviction导致写入报错 - 网络与连接数:
INFO clients关注 blocked_clients,避免大量 BRPOP 空等占满连接池 - 避免热点 key:单 key 压到单核上限(约 10 万 QPS)时,考虑本地缓存兜底或 key 打散多副本
# 实时监控命令耗时
redis-cli --latency
redis-cli --latency-history
总结
Redis 是一个工具箱,每一类问题都有对应的数据结构和策略。不要只用 String 类型,学会用合适的数据结构解决具体问题,避开 bigkey、热点 key、分布式锁这些常见的坑,才能真正发挥 Redis 的价值。缓存层面的所有设计,最终都围绕一个目标:让数据库不被打死,同时让用户看到的数据“足够新”。