数据库与缓存 列表全部文章
数据库与缓存

Redis 笔记:数据结构选型、缓存三兄弟、热 key 大 key 与生产配置

Redis 的完整实战笔记:五种基础结构的适用场景与命令,缓存穿透/击穿/雪崩的成因与解法,过期与内存淘汰策略,热 key 和大 key 的发现与处理,持久化与主从哨兵配置,附一次缓存雪崩的真实复盘。

更多

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        # 只取需要的字段,省网络

二、缓存三兄弟

穿透 查一个根本不存在的 key 每次都打到 DB 解:布隆过滤器 / 缓存空值 击穿 单个热 key 过期瞬间 大量请求同时回源 解:互斥锁 / 逻辑过期 雪崩 大批 key 同时过期 或 Redis 直接挂了 解:随机 TTL / 高可用 通用防御:过期时间加随机抖动(如 30min + random(0,300s)),避免同时失效 兜底:缓存层整体不可用时要有降级策略(本地缓存 / 直接返回默认值),不能让 DB 裸奔

穿透的两种解法:

  • 缓存空值: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 瞬间打满,接口大面积超时。

排查:

  1. 监控显示 Redis 命中率从 98% 掉到 20% —— 缓存几乎全没了。
  2. 查 Redis 日志:maxmemory 达到上限,触发 allkeys-lru 大量淘汰 —— 但那天并没有新数据写入。
  3. 再看:前一天晚上做了一次全量预热,几十万个 key 用的是固定 TTL 30 分钟,于是早上 9 点整它们集体失效。

根因:批量预热用了相同 TTL,导致同时过期;叠加征期高峰,回源量瞬间超出数据库承载能力。

处理:

  • TTL 改成 基础时间 + 随机抖动,避免集体失效。
  • 预热改成分批滚动预热,不集中在一个时间点。
  • 加多级缓存:本地 Caffeine(1 分钟)+ Redis(30 分钟),即使 Redis 全挂,本地还能挡住一波。
  • 数据库侧加回源限流:单个 key 的并发回源只允许一个(就是前面那个互斥锁),其余等待。
  • 加监控告警:命中率低于 90% 立即告警。

案例:大 key 导致主从同步卡顿

现象:主从延迟周期性飙高,从库读到的旧数据引发业务投诉。

排查:--bigkeys 发现有一个 Hash 存了 300 万字段(某全量配置表被整体塞进一个 key)。每次这个 key 有更新,同步的数据量巨大,且 HGETALL 时网络传输几百 MB。

处理:按业务维度拆成多个 Hash;读取改为分页(HSCAN);大 key 清理安排在凌晨低峰期。

八、几条硬规则

  1. key 要有业务前缀和版本:{业务}:{对象}:{id}:v1,方便排查和批量清理。
  2. 必须设 TTL,除非你非常确定它是永久数据 —— 无 TTL 的 key 是最常见的内存泄漏源。
  3. 禁止 KEYS *,用 SCAN。
  4. 批量操作用 pipeline / MGET,减少网络往返。
  5. 不要用 Redis 做消息队列(无 ack、无持久化保证),要可靠就用 RocketMQ/Kafka。
  6. 缓存一致性:更新时「先更新 DB,再删除缓存」,而不是更新缓存。删除失败要有重试(MQ 或订阅 Binlog 补偿)。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人