Redis 用得好能把 QPS 提升一个数量级,用不好会在某个夜里把数据库打穿。这篇按「选型 → 缓存设计 → 风险 → 运维」整理。
一、数据结构怎么选
| 结构 | 适用场景 | 常用命令 | 注意 |
|---|---|---|---|
| String | 单值缓存、计数器、分布式锁 | SET/GET/INCR/SETNX |
最大 512MB,别存大对象 |
| Hash | 对象缓存(用户信息、配置) | HSET/HGETALL/HMGET |
字段多时用 HMGET 取部分 |
| List | 队列、最新列表 | LPUSH/RPOP/BRPOP |
可做简单 MQ(无 ack,不可靠) |
| Set | 去重、标签、共同好友 | SADD/SISMEMBER/SINTER |
支持集合运算 |
| ZSet | 排行榜、延迟队列、优先队列 | ZADD/ZRANGEBYSCORE/ZREMRANGEBYSCORE |
最被低估的结构 |
| Bitmap | 签到、活跃标记 | SETBIT/BITCOUNT |
极省内存 |
| HyperLogLog | UV 统计(允许误差) | PFADD/PFCOUNT |
0.81% 误差,12KB 固定 |
一条重要经验:对象缓存用 Hash 而不是把对象序列化成 String。原因是能按字段读写,修改一个字段不用整体覆盖,也避免并发覆盖问题。
HSET user:1001 name "张三" age 30
HGETALL user:1001
HMGET user:1001 name age # 只取需要的字段,省网络
二、缓存三兄弟
穿透的两种解法:
- 缓存空值:DB 查不到也写
SET key "" EX 60。简单,但恶意刷不同 key 会占内存。 - 布隆过滤器:把存在的 key 预热进过滤器,不存在的直接拦截。内存极省。注意布隆过滤器有误判率且不支持删除,删除场景要用 Counting Bloom 或 Cuckoo Filter。
击穿的解法(互斥重建):
public Data get(Long id) {
Data d = redis.get(key(id));
if (d != null) return d;
String lockKey = "lock:" + id;
if (redis.set(lockKey, "1", "NX", "EX", 5) != null) { // 抢到重建锁
try {
d = db.query(id);
redis.set(key(id), d == null ? "" : d, "EX", 1800 + random(300));
} finally {
redis.del(lockKey);
}
} else {
Thread.sleep(50);
return get(id); // 没抢到,短暂等待后重读
}
return d;
}
三、过期与内存淘汰
过期删除策略(Redis 用的是组合):
- 惰性删除:访问时才检查是否过期 —— 省 CPU,但过期 key 可能长期占内存。
- 定期删除:每 100ms 随机抽样检查 —— 折中方案。
内存淘汰策略(maxmemory-policy)选型:
| 策略 | 说明 | 场景 |
|---|---|---|
noeviction |
满了就报错 | 不允许丢数据的缓存(默认,但很危险) |
allkeys-lru |
全体 key 按 LRU 淘汰 | 纯缓存首选 |
volatile-lru |
只对设了 TTL 的 key 淘汰 | 混合使用 |
allkeys-lfu |
按访问频率淘汰 | 有热点且热点稳定 |
volatile-ttl |
优先淘汰剩余时间短的 | 延迟队列类 |
配置建议:maxmemory 设为物理内存的 60%–70%(要给 RDB fork 和碎片留空间),maxmemory-policy=allkeys-lru。
四、热 key 与大 key
发现:
redis-cli --hotkeys # 需要 maxmemory-policy 是 LFU 才准确
redis-cli --bigkeys # 扫描大 key(生产慎用,会阻塞)
redis-cli --memkeys # 6.x 按内存排序
# 更安全:用 MEMORY USAGE 逐个查,或用 redis-rdb-tools 离线分析 RDB
redis-cli MEMORY USAGE user:1001
处理:
| 问题 | 解法 |
|---|---|
| 热 key | 拆成多个子 key 分散(hot:1 ~ hot:10,读时随机选一个);或本地缓存(Caffeine)+ 短 TTL |
| 大 key | 拆分(Hash 按字段分片);压缩;改用更适合的结构;删除用 UNLINK 不要 DEL |
大 key 删除会阻塞主线程(DEL 一个百万元素的 ZSet 能让 Redis 卡几秒)。Redis 4.0+ 用 UNLINK(异步删除)。
五、持久化与高可用
| 方式 | 原理 | 取舍 |
|---|---|---|
| RDB | 定时快照 | 文件小、恢复快,可能丢最后一次快照后的数据 |
| AOF | 追加写命令日志 | 数据安全(最多丢 1s),文件大、恢复慢 |
| 混合持久化(推荐) | AOF 里存 RDB 格式的全量 + 增量命令 | 兼顾两者,4.0+ 支持 |
# redis.conf 关键项
appendonly yes
aof-use-rdb-preamble yes # 混合持久化
save 900 1
maxmemory 4gb
maxmemory-policy allkeys-lru
# 主从
replicaof 10.0.0.11 6379
# 安全
requirepass <强密码>
rename-command FLUSHALL "" # ★ 禁用危险命令
生产必做:禁用 KEYS(用 SCAN 代替)、FLUSHALL、FLUSHDB。KEYS * 在生产上执行一次可能让 Redis 卡死几秒。
主从 + 哨兵:一主两从 + 三个哨兵是最小的可靠配置。集群(Cluster)用于数据量超过单机内存的场景。
六、分布式锁(Redisson 是正解)
RLock lock = redissonClient.getLock("order:" + orderId);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // 等 5s,锁 30s 自动释放
doBiz();
}
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
自己用 SETNX 实现锁容易漏三件事:原子释放(要用 Lua 脚本先校验 value 再删)、锁续期(看门狗)、可重入。Redisson 都做好了,不要自己造。
七、生产实践
案例:一次缓存雪崩
背景:某财税 SaaS 公司的申报清册服务,征期(每月 1–15 日)流量是平时的十几倍。
现象:某月征期第一天上午 9 点,数据库 CPU 瞬间打满,接口大面积超时。
排查:
- 监控显示 Redis 命中率从 98% 掉到 20% —— 缓存几乎全没了。
- 查 Redis 日志:
maxmemory达到上限,触发allkeys-lru大量淘汰 —— 但那天并没有新数据写入。 - 再看:前一天晚上做了一次全量预热,几十万个 key 用的是固定 TTL 30 分钟,于是早上 9 点整它们集体失效。
根因:批量预热用了相同 TTL,导致同时过期;叠加征期高峰,回源量瞬间超出数据库承载能力。
处理:
- TTL 改成
基础时间 + 随机抖动,避免集体失效。 - 预热改成分批滚动预热,不集中在一个时间点。
- 加多级缓存:本地 Caffeine(1 分钟)+ Redis(30 分钟),即使 Redis 全挂,本地还能挡住一波。
- 数据库侧加回源限流:单个 key 的并发回源只允许一个(就是前面那个互斥锁),其余等待。
- 加监控告警:命中率低于 90% 立即告警。
案例:大 key 导致主从同步卡顿
现象:主从延迟周期性飙高,从库读到的旧数据引发业务投诉。
排查:--bigkeys 发现有一个 Hash 存了 300 万字段(某全量配置表被整体塞进一个 key)。每次这个 key 有更新,同步的数据量巨大,且 HGETALL 时网络传输几百 MB。
处理:按业务维度拆成多个 Hash;读取改为分页(HSCAN);大 key 清理安排在凌晨低峰期。
八、几条硬规则
- key 要有业务前缀和版本:
{业务}:{对象}:{id}:v1,方便排查和批量清理。 - 必须设 TTL,除非你非常确定它是永久数据 —— 无 TTL 的 key 是最常见的内存泄漏源。
- 禁止
KEYS *,用SCAN。 - 批量操作用 pipeline / MGET,减少网络往返。
- 不要用 Redis 做消息队列(无 ack、无持久化保证),要可靠就用 RocketMQ/Kafka。
- 缓存一致性:更新时「先更新 DB,再删除缓存」,而不是更新缓存。删除失败要有重试(MQ 或订阅 Binlog 补偿)。
一起聊聊