← 返回场景决策

🔴 Redis 能力与生产实战

结果导向 · 能力全景 · 数据结构 / 分布式锁 / 排行榜 / 持久化 / 高可用

String Hash ZSet 分布式锁 排行榜 限流 持久化 高可用

1. 直接给结论

Redis 不只是缓存,是个内存数据库

大多数人只知道用 Redis 做缓存,但它还能做分布式锁、排行榜、限流、消息队列、计数器、会话存储。选 Redis 的核心理由:① 快(内存级)② 数据结构丰富(不止 KV)③ 单线程无并发问题 ④ 支持持久化(断电不丢)。

你的需求用 Redis 的什么一句话原因
缓存热点数据String / Hash内存级速度,减轻 DB 压力(详见缓存专题
多机互斥操作(防重复执行)分布式锁(SETNX / Redisson)SET key NX EX — 原子加锁,过期自动释放
排行榜 / Top NZSet(有序集合)自带排序,ZADD/ZREVRANGE 秒级出榜
API 限流(每分钟 100 次)计数器(INCR + EXPIRE)原子自增 + 过期自动清零,跨机器共享
统计 UV / 去重HyperLogLog / Set12KB 统计亿级 UV,误差 0.81%
消息队列(轻量)Stream / ListStream 支持消费组/ACK,比 MQ 轻量
用户签到 / 在线状态Bitmap1 个用户 1 bit,1 亿用户只要 12MB
附近的人 / 门店GeoGEORADIUS 按坐标查附近
分水岭就一个:你的数据要不要多台机器共享 + 快速读写
• 要 → Redis(独立进程,所有机器访问同一份)
• 不要 + 只要单机 → Caffeine 本地缓存(纳秒级,不走网络)
• 要持久化 + 复杂查询 → MySQL(磁盘级,但支持 SQL 和事务)
什么时候不要用 Redis
• 数据量超过内存(Redis 是内存数据库,放不下就得淘汰或分片,成本高)
• 需要复杂 SQL 查询(Redis 没有 WHERE/JOIN/聚合)
• 强一致性事务(Redis 事务弱,不支持回滚)
• 冷数据(很少访问的数据放 Redis 是浪费内存,放 MySQL)

2. Redis 能做什么(能力全景)

Redis 的能力远不止"缓存"。它有4 大能力域

① 丰富的数据结构
不止 KV,有 9 种结构:String/Hash/List/Set/ZSet/Bitmap/HyperLogLog/Stream/Geo。每种结构对应一类业务场景,不用自己造轮子
② 持久化
虽然是内存数据库,但能落盘不丢数据。RDB 快照 + AOF 日志两种方式,重启后自动恢复。
③ 高可用
主从复制 + 哨兵自动故障转移 + 集群分片。单点挂了自动切,数据分片到多台扛海量数据。
④ 单线程高性能
单线程模型无并发问题(不用锁),纯内存操作 10 万+ QPS。6.0 后网络 IO 多线程,命令仍单线程。

能力一览表:

能力能做什么对应技术类比
缓存热点数据存内存,减轻 DB 压力String/Hash + TTL桌上放常用笔,不用每次开柜子
分布式锁多台机器互斥执行SETNX / Redisson公共厕所的门锁,进去就锁上
排行榜实时排序,Top NZSet考试成绩排名表,自动排序
限流API 每分钟限 N 次INCR + EXPIRE地铁进站口的闸机,限流放行
计数器点赞数/库存/浏览量INCR/DECR取号机,按一下加一个号
消息队列异步任务/解耦Stream/List快递传送带,生产者放上去消费者取
UV 统计亿级去重计数HyperLogLog用指纹代替真人,估算人数
签到/在线用户签到/在线状态Bitmap考勤打卡表,每个人每天一个格
附近搜索附近的人/门店Geo地图上画个圈,圈内的都找出来
会话存储用户登录 Session 共享String + TTL寄存处,存个牌号凭条取物
一句话总结 Redis = 内存数据库 + 9 种数据结构 + 持久化 + 高可用。大多数人只用它做缓存,浪费了 80% 的能力。分布式锁、排行榜、限流、消息队列这些场景,用 Redis 比引入专门中间件更轻量。

3. 数据结构能力(Redis 的灵魂)

Redis 强在哪?9 种数据结构,每种对应一类业务场景。用对结构,代码少一半,性能高十倍。

结构是什么典型场景复杂度
String最简单的 KV,value 是字符串/数字缓存/计数器/分布式锁/SessionO(1)
Hash一个 key 下存多个字段(像对象)商品详情/用户信息(比 String 省内存)O(1)
List有序列表,两头可插删消息队列/最新动态/操作记录O(1) 头尾
Set无序集合,自动去重标签/共同好友/去重O(1)
ZSet有序集合,按 score 排序排行榜/延迟队列/Top NO(logN)
Bitmap位图,1 个用户 1 bit签到/在线状态/布隆过滤器O(1)
HyperLogLog基数估算(去重计数)UV 统计(亿级去重)O(1)
Stream消息流(5.0+)消息队列/事件流O(1)
Geo地理坐标附近的人/门店/距离O(logN)

💻 5 大基础结构 redis-cli 实操

① String — 缓存 / 计数器 / 分布式锁

127.0.0.1:6379> SET user:1001 '{"name":"张三","age":25}' # 缓存用户 OK 127.0.0.1:6379> GET user:1001 "{\"name\":\"张三\",\"age\":25}" 127.0.0.1:6379> INCR article:88:likes # 点赞 +1(原子操作) (integer) 1 127.0.0.1:6379> INCR article:88:likes # 再点一次 (integer) 2 127.0.0.1:6379> SET lock:order:10086 "node-1" NX EX 30 # 分布式锁:不存在才设,30秒过期 OK # 加锁成功 127.0.0.1:6379> SET lock:order:10086 "node-2" NX EX 30 # 另一台机器再试 (nil) # 加锁失败,已被占用

② Hash — 对象存储(比 String 省内存)

# 存商品详情:一个 key 下多个字段 127.0.0.1:6379> HSET product:10086 name "iPhone" price 6999 stock 100 (integer) 3 127.0.0.1:6379> HGET product:10086 price # 只取价格 "6999" 127.0.0.1:6379> HINCRBY product:10086 stock -1 # 库存 -1(原子) (integer) 99 127.0.0.1:6379> HGETALL product:10086 # 取全部 name "iPhone" price "6998" stock "99"

③ ZSet — 排行榜(核心能力)

# 游戏积分排行榜:ZADD 加成员+分数,自动按分数排序 127.0.0.1:6379> ZADD rank:game 8500 "玩家A" 9200 "玩家B" 7800 "玩家C" (integer) 3 127.0.0.1:6379> ZREVRANGE rank:game 0 2 WITHSCORES # Top3(从高到低) "玩家B" "9200" "玩家A" "8500" "玩家C" "7800" 127.0.0.1:6379> ZINCRBY rank:game 500 "玩家A" # 玩家A加分 "9000" 127.0.0.1:6379> ZRANK rank:game "玩家A" # 玩家A排名(从0开始) (integer) 0 # 第1名!

④ List — 消息队列(轻量)

# 生产者:LPUSH 从左边塞消息 127.0.0.1:6379> LPUSH task:queue "发邮件:10086" "发短信:10087" (integer) 2 # 消费者:BRPOP 从右边取(阻塞式,没消息就等) 127.0.0.1:6379> BRPOP task:queue 0 # 0 = 一直等 "task:queue" "发邮件:10086" # 先进先出

⑤ Bitmap — 签到(极致省内存)

# 用户1001,6月18日签到(第18天 → offset 18) 127.0.0.1:6379> SETBIT sign:1001:202606 18 1 (integer) 0 127.0.0.1:6379> GETBIT sign:1001:202606 18 # 查18号签了没 (integer) 1 127.0.0.1:6379> BITCOUNT sign:1001:202606 # 本月签到几天 (integer) 1 # 1亿用户 × 31天 = 31亿 bit ≈ 387MB(用 String 存要几十GB)
选结构的口诀
• 存对象/多个字段 → Hash(比 String 省 50% 内存)
• 要排序/排行榜 → ZSet(自带排序,不用查 DB ORDER BY)
• 要去重 → Set(小数据)或 HyperLogLog(大数据,12KB 估算亿级)
• 要队列 → Stream(5.0+,支持消费组)或 List(简单场景)
• 要省内存的签到/在线 → Bitmap(1 用户 1 bit)

4. 持久化与高可用(断电不丢 + 不停机)

💾 持久化:RDB vs AOF

RDB(快照)AOF(日志)
原理定期把内存全量数据 dump 成 .rdb 文件把每条写命令追加到 .aof 日志
恢复速度快(直接加载文件)慢(要重放所有命令)
数据丢失可能丢最近几分钟最多丢 1 秒(everysec 策略)
文件大小小(压缩二进制)大(文本命令,需定期重写)
性能影响小(fork 子进程异步做)中(每次写都要记日志)
适合备份/灾备/可接受少量丢失数据安全要求高
生产推荐:RDB + AOF 混合。Redis 4.0+ 支持:AOF 重写时把当前内存做 RDB 快照写到 AOF 头部,后续增量命令追加。兼顾恢复速度和数据安全。

🏗️ 高可用:主从 / 哨兵 / 集群

方案原理能干啥复杂度
主从复制一主多从,主写从读,数据自动同步读写分离/数据备份
哨兵 Sentinel主从 + 监控进程,主挂了自动选新主自动故障转移(不用人工切)
集群 Cluster数据分片到多台(16384 槽位)横向扩展/海量数据/高并发
Master Slave 1 + Slave 2
主从复制:主负责写,从负责读,数据自动同步
哨兵 × 3 监控 Master 挂了? → Slave 1 升主
哨兵:监控主节点,挂了自动把从升为主,应用无感知
怎么选:① 数据量小 + 要高可用 → 主从 + 哨兵(1 主 2 从 + 3 哨兵)② 数据量大(>100GB)/并发高 → 集群(6 节点起,3 主 3 从)③ 单机够用 + 能接受短暂中断 → 单机 + AOF

5. Redis 监控面板长这样

生产环境的 Redis 通常用 RedisInsight(官方 GUI)或 Grafana 接 Prometheus 监控。下面模拟一个生产监控面板:

🔴 Redis 监控 概览 慢查询 大 key 热 key 实时 · 自动刷新 5s
内存使用
2.4 GB
/ 4 GB 上限 (60%)
连接数
421
/ 10000 上限
QPS
12,341
健康
命中率
94.2%
健康(>80%)
平均延迟
0.8ms
优秀(<1ms)
慢查询
3
⚠️ 超过 10ms
key 总数
482,193
已设置 TTL
主从延迟
0.1ms
同步正常
QPS / 内存 — 最近 1 小时趋势
QPS 内存 (GB)
关键监控指标
  • 内存使用率:超过 80% 要警惕(Redis 满了会触发淘汰或拒绝写入)
  • 命中率:低于 80% 说明缓存策略有问题(看缓存专题)
  • 慢查询:超过 10ms 的命令要查(可能是 KEYS/大 key/复杂操作)
  • 主从延迟:超过 1s 说明同步有问题(网络/大 key/写压力大)

6. 通常和什么搭配使用

搭配是啥为啥搭配
Spring Data RedisJava 操作 Redis 的库封装好 RedisTemplate/StringRedisTemplate,注解化操作
Redisson分布式锁框架比手写 SETNX 更可靠 — 自动续期/可重入/红锁
Caffeine本地缓存多级缓存 L1(Caffeine) + L2(Redis),见缓存专题
Prometheus + Grafana监控Redis Exporter 采集指标 → Grafana 看板(上面的监控面板就是这套)
MySQL持久数据库Redis 做缓存/计算层,MySQL 做持久化层,数据双写
Kafka/RabbitMQ专业消息队列Redis Stream 做轻量队列,专业场景用 Kafka/MQ

💻 Spring Boot 操作 Redis

RedisService.java
📁 service/
RedisService.java
📁 config/
RedisConfig.java
@Service public class RedisService { @Autowired private StringRedisTemplate redis; // ① String:缓存 public void cacheUser(String userId, User user) { redis.opsForValue().set("user:" + userId, toJSON(user), 10, TimeUnit.MINUTES); } // ② Hash:商品详情 public void cacheProduct(Long id, Map<String, String> fields) { redis.opsForHash().putAll("product:" + id, fields); } // ③ ZSet:排行榜 public void addScore(String gameId, String player, double score) { redis.opsForZSet().incrementScore("rank:" + gameId, player, score); } public Set<String> topN(String gameId, int n) { return redis.opsForZSet().reverseRange("rank:" + gameId, 0, n - 1); } // ④ 分布式锁(用 Redisson 更可靠) public boolean tryLock(String key, long ttl) { Boolean ok = redis.opsForValue().setIfAbsent("lock:" + key, "1", ttl, TimeUnit.SECONDS); return Boolean.TRUE.equals(ok); } // ⑤ 限流:每分钟 N 次 public boolean allowRequest(String userId, int limit) { String key = "rate:" + userId; Long count = redis.opsForValue().increment(key); if (count == 1) redis.expire(key, 60, TimeUnit.SECONDS); return count <= limit; } }

7. 生产上怎么用(3 个典型场景,带界面演示)

🎬 场景 1:分布式锁 — 防止库存超卖

现象:秒杀场景,10 件商品,1000 人同时下单。如果不加锁,3 台机器各自查到库存=10,各卖 10 件 → 超卖 20 件。需要分布式锁:同一时刻只有 1 个请求能扣库存。

Redis 监控面板:分布式锁实时状态

🔴 Redis lock:* (分布式锁) 秒杀进行中 ▾
lock:stock:10086 TTL: 28s 持有者: node-2 🔒
lock:stock:10087 已释放 ✓ 下一个等待中...
锁等待
823
个请求在排队
获取成功
1
当前持有锁
已处理
10/10
库存扣完
超卖
0
✅ 锁保护成功
实现(Redisson,生产推荐)
// Redisson 分布式锁(比手写 SETNX 可靠:自动续期/可重入/防死锁) RLock lock = redissonClient.getLock("lock:stock:" + productId); try { if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 等3秒,锁30秒自动释放 int stock = Integer.parseInt(redis.opsForValue().get("stock:" + productId)); if (stock > 0) { redis.opsForValue().decrement("stock:" + productId); // 创建订单... } } } finally { if (lock.isHeldByCurrentThread()) lock.unlock(); }
为什么用 Redisson 不用手写 SETNX:① 自动续期(业务没执行完锁不会过期)② 可重入(同线程可多次获取)③ 释放锁用 Lua 脚本(原子性,防误删)

🎬 场景 2:排行榜 — 实时游戏积分榜

现象:游戏要实时显示积分 Top 100,每周一清零重排。如果用 MySQL ORDER BY,100 万玩家排序要几秒;用 Redis ZSet,毫秒级返回。

Redis 排行榜实时面板

🔴 Redis rank:game (ZSet) Top 5 ▾
ZREVRANGE rank:game 0 4 WITHSCORES
1 王者归来 12,450
2 无敌战神 11,820
3 深夜刺客 10,950
4 咖啡程序员 9,800
5 新手小白 9,120
榜单人数
1,243,891
ZSet 成员数
查询耗时
2ms
ZREVRANGE
内存占用
78MB
比 MySQL 省 10x
更新 QPS
8,234
ZINCRBY/s
实现
// 玩家加分(实时更新排行榜) redis.opsForZSet().incrementScore("rank:game", playerId, score); // 查 Top 100 Set top100 = redis.opsForZSet().reverseRange("rank:game", 0, 99); // 查我的排名 Long myRank = redis.opsForZSet().reverseRank("rank:game", myId); // 每周一清零:直接删 key 重建 redis.delete("rank:game");
为什么不用 MySQL:100 万行 ORDER BY 要 2-5 秒,Redis ZSet 的 ZREVRANGE 是 O(logN+M),2ms 返回。ZSet 底层是跳表 + 哈希表,天然有序。

🎬 场景 3:API 限流 — 每分钟 100 次

现象:对外 API 要限流"每个用户每分钟最多调 100 次"。3 台机器各自计数没意义(加起来超了),需要 Redis 集中计数。

Redis 限流计数器面板

🔴 Redis rate:* (限流) 当前分钟 ▾
rate:user:10086 100/100 🚫 已限流
rate:user:10087 42/100 ✅ 正常
rate:user:10088 89/100 ⚠️ 接近上限
被限流请求
1,234
本分钟
放行请求
42,891
本分钟
限流用户数
23
超 100 次/分
TTL 自动清零
60s
每分钟重置
实现(计数器法)
public boolean allowRequest(String userId, int limit) { String key = "rate:" + userId; Long count = redis.opsForValue().increment(key); // +1(原子) if (count == 1) { redis.expire(key, 60, TimeUnit.SECONDS); // 第一次设过期,60秒后自动清零 } return count <= limit; // ≤100 放行,>100 拒绝 }
进阶:① 滑动窗口限流(用 ZSet,更精准)② 令牌桶(Redisson RateLimiter,支持突发流量)③sentinel/guava 限流框架

8. 冷热数据管理(预热 / 筛选 / 分层)

为什么要管冷热数据

Redis 是内存数据库,内存贵且有限。把所有数据都塞 Redis = 烧钱;把冷数据也放 Redis = 浪费内存 + 命中率低。冷热分离是 Redis 生产化的核心能力:热数据放 Redis 秒级响应,冷数据归档到 MySQL/对象存储省内存。这一章讲清楚:怎么识别热数据、怎么预热、冷数据怎么处理。

🌡️ 什么是冷热数据

分类定义访问频率放哪典型例子
🔥 热数据频繁访问,对延迟敏感每秒/每分钟访问Redis首页商品/热搜词/在线用户Session/秒杀库存
🍵 温数据偶尔访问,可容忍百ms每天访问几次Redis(设TTL)或 MySQL+本地缓存用户最近7天订单/商品详情/配置项
❄️ 冷数据很少访问,秒级可接受每月/几乎不访问MySQL / 对象存储 / 归档表历史订单/日志/3个月前的消息/已结束活动
冷热判断的一句话标准"这个数据如果不在 Redis,用户能不能忍 100ms?" 能忍 → 冷数据,放 MySQL;不能忍 → 热数据,放 Redis。秒杀库存、热搜、首页 Banner 这种不能忍的才进 Redis,其他该放 MySQL 放 MySQL。

🔍 怎么识别/筛选热数据(4 种方法)

热数据不是拍脑袋定的,要用数据说话。生产里有 4 种识别方法,从简单到专业:

方法 ①:业务侧直接标注(最简单,凭经验)

// 启动时把已知的热数据批量加载到 Redis @PostConstruct public void preloadHotData() { // 1. 首页 Banner(运营配置,肯定是热的) List<Banner> banners = bannerMapper.findActive(); for (Banner b : banners) { redis.opsForValue().set("banner:" + b.getId(), toJSON(b), 1, TimeUnit.HOURS); } // 2. 秒杀商品库存(大促前运营指定) List<Long> seckillIds = seckillMapper.findOngoingIds(); for (Long id : seckillIds) { Integer stock = productMapper.getStock(id); redis.opsForValue().set("stock:" + id, String.valueOf(stock)); } }
适用场景:数据量小、热数据明确(首页/秒杀/热搜)。缺点:靠人判断,可能漏掉隐性热点。适合项目初期。

方法 ②:统计访问频次(最常用,数据驱动)

给每个 key 加访问计数,定期统计 TOP N 就是热数据。两种实现:

// 方式A:用 Redis ZSet 统计 key 访问次数(轻量) public Object getWithStat(String key) { Object val = redis.opsForValue().get(key); if (val != null) { // 命中就 +1 访问计数 redis.opsForZSet().incrementScore("__hot_keys__", key, 1); } return val; } // 定时任务:每天凌晨统计昨天的 TOP 100 热key,加入预热清单 @Scheduled(cron = "0 0 2 * * ?") public void analyzeHotKeys() { Set<String> top100 = redis.opsForZSet().reverseRange("__hot_keys__", 0, 99); // 存到配置表,下次启动时预热这些 key hotKeyMapper.batchSave(top100); redis.delete("__hot_keys__"); // 清空,重新统计 }
适用场景:数据量大、热点不明显、需要数据支撑决策。这是生产最常用的方法。缺点:统计本身也消耗一点 Redis 性能。

方法 ③:Redis 自带 hotkey 发现(redis-cli --hotkeys)

# Redis 4.0+ 自带 hotkey 发现功能(基于 LFU 计数) $ redis-cli --hotkeys # 输出示例(按访问频次排序) Sampling 1000000 keys for hot keys... --------------------------------------- hotKey found: 1 [1] product:10086 (accesses: 82341) # ← 这就是热 key! hotKey found: 2 [2] user:session:1001 (accesses: 45892) hotKey found: 3 [3] rank:game (accesses: 31204) hotKey found: 4 [4] banner:home (accesses: 18923) hotKey found: 5 [5] config:global (accesses: 15402) --------------------------------------- # 注意:要先开启 maxmemory-policy = allkeys-lfu 才能用
PRO TIP redis-cli --hotkeys 基于 LFU 计数器,不需要额外代码,Redis 自己就在记录每个 key 的访问频次。前提是淘汰策略要配成 allkeys-lfuvolatile-lfu这是发现热点最快的方法,排查"哪个 key 打爆了 Redis"时第一时间用它。

方法 ④:监控采样 + 链路分析(最专业)

redis-faina(Twitter 开源)或 RedisInsight 分析 MONITOR 采样的命令流,统计热点 key/热点命令。适合大厂精细运营。

# redis-faina 分析 MONITOR 抓到的命令流 $ redis-cli MONITOR | redis-faina --timeout 30 Overall Top Keys ======================================= 1. product:10086 (18234 calls, 12.4%) # ← 占总命令12%!典型热key 2. user:1001 (8421 calls, 5.7%) 3. session:abc123 (6234 calls, 4.2%) 4. cart:10086 (3120 calls, 2.1%) 5. stock:999 (2890 calls, 2.0%) Overall Top Commands ======================================= 1. GET (82341 calls, 56.2%) 2. SET (32104 calls, 21.9%) 3. EXPIRE (15402 calls, 10.5%)

🔥 怎么预热(启动时加载热数据)

预热 = 服务启动时把热数据提前加载到 Redis,避免冷启动时大量请求直接打 DB。有 3 种策略:

策略怎么做适合场景缺点
全量预热启动时把所有"可能热"的数据全灌进 Redis数据量小(<10万)/ 活动前准备启动慢,可能灌了一堆冷数据
增量预热只预热统计出来的 TOP N 热 key数据量大 / 日常运行需先有统计数据
懒加载不预热,第一次访问时查 DB 回填 Redis数据量大 + 冷启动可接受首批用户慢,可能击穿
生产推荐组合:增量预热 + 懒加载兜底。启动时预热统计出的 TOP 100 热 key(覆盖大部分流量),其余的第一次访问时懒加载回填。不要全量预热——大部分数据根本不会被访问,灌进去纯浪费内存。

🎬 场景 1:大促前预热秒杀商品(全量预热)

背景:双 11 零点秒杀,预计 100 万人抢 10 款商品。如果零点才开始查 DB 加载商品信息和库存到 Redis,前几秒 DB 会被打爆。需要提前预热:活动开始前 1 小时把商品数据、库存、活动配置全灌进 Redis。

预热过程监控面板

🔴 Redis 预热进度 热 key 榜 大促前 1 小时 ▾
已预热 key
8,421
/ 10,000 目标
预热进度
84%
预计还需 8 分钟
当前 QPS
176
预热写入
内存使用
1.8 GB
/ 4 GB
预热任务清单(分批执行):
✓ 批次1: 秒杀商品详情 ×10 完成 10/10
✓ 批次2: 库存数据 ×10 完成 10/10
⏳ 批次3: 活动配置 ×5,000 4,201/5,000
○ 批次4: 用户优惠券 ×50,000 等待中
已加载的 TOP 5 热 key:
1stock:10086预估 50万 QPS
2product:10086预估 30万 QPS
3seckill:config预估 20万 QPS
4banner:home预估 8万 QPS
5coupon:rules预估 5万 QPS
预热代码(分批加载,避免一次性打爆 DB)
@Scheduled(cron = "0 0 23 * * ?") // 大促前一天23点执行 public void preheatForSeckill() { List<Long> seckillIds = seckillMapper.findTomorrowIds(); // 分批加载,每批 100 条,避免一次性查 DB 几万条把 DB 搞挂 Lists.partition(seckillIds, 100).forEach(batch -> { for (Long id : batch) { // 1. 商品详情(Hash,TTL 2小时) Product p = productMapper.findById(id); Map<String, String> fields = toBeanMap(p); redis.opsForHash().putAll("product:" + id, fields); redis.expire("product:" + id, 2, TimeUnit.HOURS); // 2. 库存(String,不设TTL,活动结束才删) Integer stock = stockMapper.getStock(id); redis.opsForValue().set("stock:" + id, String.valueOf(stock)); } try { Thread.sleep(100); } catch (Exception e) {} // 每批间隔100ms,别把DB压垮 }); log.info("预热完成,共加载 {} 个秒杀商品", seckillIds.size()); }

🎬 场景 2:日常运行时热点 key 发现与自动保护

背景:日常运行中,某个商品突然爆火(比如被网红带货),QPS 从 100 飙到 5 万,这个 key 成了热点,单分片被打爆。需要自动发现热点 key + 自动加本地缓存保护

热点 key 实时监控面板(redis-cli --hotkeys)

🔴 Redis 热点 key 实时榜 冷 key 归档 最近 5 分钟 ▾
🔍 redis-cli --hotkeys 采样结果(按 LFU 访问计数排序):
1 product:10086 82,341 次 ⚠️ 爆热!已触发本地缓存
2 product:10087 45,892 次 ⚠️ 接近阈值
3 banner:home 18,923 次 ✅ 正常热点
4 config:global 15,402 次 ✅ 正常
5 user:profile:9999 3 次 ❄️ 冷 key,建议归档
爆热 key
1
>5万次/5分钟
接近阈值
1
1-5万次
已本地缓存
1
自动保护
冷 key 待归档
12,483
访问<3次/天
热点 key 自动保护(本地缓存兜底)
// 1. 定时扫描热点 key(每分钟) @Scheduled(fixedRate = 60000) public void scanHotKeys() { // 用 OBJECT FREQ 命令查 key 的 LFU 访问频次(Redis 4.0+) for (String key : scanAllKeys()) { Long freq = redis.execute(c -> c.objectFreq(key)); if (freq != null && freq > 50000) { // 2. 超过5万次 → 标记为热点,加入本地缓存白名单 hotKeyManager.markHot(key); log.warn("发现热点 key: {},访问 {} 次,已启用本地缓存保护", key, freq); } } } // 3. 读取时优先走本地缓存(Caffeine),命中就不打 Redis public Object get(String key) { // 如果是热点 key,先查本地缓存 if (hotKeyManager.isHot(key)) { Object local = caffeine.getIfPresent(key); if (local != null) return local; // 本地命中,不打 Redis } // 非热点或本地未命中 → 查 Redis Object val = redis.opsForValue().get(key); if (val != null && hotKeyManager.isHot(key)) { caffeine.put(key, val); // 回填本地缓存(TTL 10秒,防数据太旧) } return val; }

🎬 场景 3:冷数据自动归档(TTL + 淘汰)

背景:用户消息表 3 个月积累了 5000 万条,全放 Redis 要 20GB,成本太高。实际上 3 个月前的消息几乎没人看。方案:近 7 天消息放 Redis,7-90 天放 MySQL,90 天以上归档到冷存储

🔥 0-7天 Redis | 🍵 7-90天 MySQL 热库 | ❄️ >90天 MySQL 归档表/对象存储
冷热分层:热数据秒级响应,冷数据省内存省钱

冷数据归档任务面板

🔴 Redis 冷 key 归档 热 key 榜 归档任务执行中 ▾
待归档 key
48,231
7天未访问
已归档
12,489
已迁到 MySQL
释放内存
1.2 GB
本轮释放
归档速度
2,400
key/分钟
归档策略配置:
hot_ttl: 7天 → Redis (access > 100/天)
warm_ttl: 90天 → MySQL 热库 (1-100次/天)
cold_archive: >90天 → MySQL 归档表 (<1次/天)
归档日志(最近 5 条):
[02:15:03] msg:user:10086 → MySQL(msg_archive_202603)
[02:15:03] msg:user:10087 → MySQL(msg_archive_202603)
[02:15:04] msg:user:10088 → MySQL(msg_archive_202603)
[02:15:04] msg:user:10089跳过(仍在访问)
[02:15:05] msg:user:10090 → MySQL(msg_archive_202603)
归档代码(定时任务 + TTL 配合)
// 1. 写入时设 TTL,让 Redis 自动清理冷数据 public void saveMessage(Long userId, Message msg) { redis.opsForValue().set("msg:" + userId, toJSON(msg), 7, TimeUnit.DAYS); // 同时写 MySQL(持久化) messageMapper.insert(msg); } // 2. 定时归档:把 Redis 里7天未访问的 key 迁到 MySQL 归档表 @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void archiveColdData() { // SCAN 扫描所有 msg:* key(别用 KEYS!) ScanOptions opts = ScanOptions.scanOptions().match("msg:*").count(1000).build(); try (Cursor<String> cursor = redis.scan(opts)) { while (cursor.hasNext()) { String key = cursor.next(); // 查空闲时间(多久没被访问) Long idleTime = redis.execute(c -> c.objectIdletime(key)); if (idleTime != null && idleTime > 7 * 24 * 3600) { // 空闲超过7天 → 归档到 MySQL,删除 Redis String val = redis.opsForValue().get(key); archiveMapper.insert(parseMsg(val)); redis.delete(key); } } } } // 3. 读取时按层级查:Redis → MySQL热库 → MySQL归档表 public Message getMessage(Long userId) { // 先查 Redis(热数据) String val = redis.opsForValue().get("msg:" + userId); if (val != null) return parseMsg(val); // 再查 MySQL 热库(温数据) Message msg = messageMapper.findByUserId(userId); if (msg != null) { redis.opsForValue().set("msg:" + userId, toJSON(msg), 7, TimeUnit.DAYS); return msg; } // 最后查归档表(冷数据,慢但省内存) return archiveMapper.findByUserId(userId); }

📊 冷热数据管理实践总结

阶段做什么工具/命令频率
识别找出哪些是热数据redis-cli --hotkeys / ZSet 计数 / redis-faina每天/实时
预热启动/活动前加载热数据@PostConstruct / @Scheduled 分批加载启动时/活动前
保护热点 key 加本地缓存兜底Caffeine + 热点检测 + 自动降级实时
归档冷数据从 Redis 迁到 MySQLSCAN + OBJECT IDLETIME + 定时任务每天凌晨
淘汰Redis 满了自动淘汰冷 keymaxmemory + allkeys-lfu 策略自动
PRO TIP 冷热分离的核心不是技术,是"舍得删"。很多团队 Redis 越来越大、越来越慢,就是因为"万一以后用到"不肯删冷数据。生产原则:设 TTL 让 Redis 自动清理 + 定时归档 + LFU 淘汰策略兜底。冷数据放 MySQL 查询慢一点(100ms vs 1ms),但 Redis 内存省下来能扛更多热数据,整体命中率反而更高。

9. 生产坑与事故

坑 1:大 key 阻塞 Redis(单线程的噩梦)

现象:某接口偶发超时,Redis 延迟从 1ms 飙到 200ms。

原因:有人存了 50MB 的 JSON 到一个 key(all_products)。Redis 单线程,读写这个大 key 会阻塞所有其他操作 200ms。

解法:大 key 拆分 — all_productsproducts:cat:1 / products:cat:2,每个 <10KB。

教训:Redis 单 key 别超过 10KB(String)或 1 万元素(Hash/List/Set)。大 key 是 Redis 性能头号杀手。

坑 2:用了 KEYS 命令,Redis 卡死

现象:开发在线上执行 KEYS user:*,Redis 直接卡死 5 秒,所有请求超时。

原因KEYS 是 O(N) 遍历所有 key,100 万个 key 要几秒。单线程模型下,这一条命令阻塞所有操作。

解法:用 SCAN 替代(游标式遍历,不阻塞)。

教训生产环境永远不要用 KEYS。用 SCAN 替代。同理避免 FLUSHALLFLUSHDB

坑 3:缓存与 DB 不一致(卖了 1 块收了 100 块)

现象:商品改价 1 元 → 100 元,用户下单还是 1 元。

原因:先改 DB 再删缓存,删缓存失败 → 缓存还是旧价。用户按旧价下单,DB 已是新价 → 不一致。

解法:延迟双删(先删缓存 → 改 DB → sleep 500ms → 再删一次)/ 最终一致(MQ 异步删缓存,失败重试)。

教训:缓存一致性是最终一致,不是强一致。核心交易数据别缓存,或用极短 TTL。详见缓存专题

坑 4:AOF 文件撑爆磁盘

现象:Redis 写入很慢,排查发现磁盘 99% 满。

原因:AOF 日志只追加不删,运行半年后 AOF 文件 200GB。没开自动重写(auto-aof-rewrite)。

解法:手动 BGREWRITEAOF 压缩 → 配 auto-aof-rewrite-percentage 100 自动重写。

教训:AOF 必须配自动重写。监控 AOF 文件大小,超过阈值告警。

10. FAQ

Redis 为什么这么快?
  1. 纯内存操作:数据在内存,纳秒级访问(磁盘是毫秒级)
  2. 单线程:无锁无竞争,无上下文切换开销
  3. IO 多路复用:epoll 一个线程处理几万连接
  4. 高效数据结构:SDS/跳表/哈希表,针对性能优化

6.0 后网络 IO 改成多线程(解析/回写),但命令执行仍单线程 — 避免并发问题的同时提升网络吞吐

Redis 单线程为什么能扛 10 万 QPS?

因为快。纯内存操作纳秒级,单线程 1 秒能做上亿次操作。10 万 QPS 对单线程来说只用了 0.1% 的 CPU 时间。瓶颈不在 CPU,在网络 IO 和内存带宽

如果 QPS 超过单机极限(通常 10-20 万),用集群分片,数据分散到多台。

什么时候用 Redis Stream,什么时候用 Kafka/RabbitMQ?

用 Redis Stream:① 消息量不大(<10 万/秒)② 不想引入额外中间件 ③ 已有 Redis ④ 需要消费组/ACK 但场景简单

用 Kafka:① 消息量大(百万/秒)② 需要持久化可靠 ③ 大数据/日志收集场景

用 RabbitMQ:① 复杂路由 ② 业务消息(订单/支付)③ 需要确认机制

简单说:Redis Stream 是"够用就行",Kafka/MQ 是"专业选手"。

Redis 内存满了怎么办?

配置 maxmemory 上限,满了触发淘汰策略

  • noeviction:不淘汰,写入直接报错(默认,生产慎用)
  • allkeys-lru:淘汰最久没用的(缓存常用)
  • allkeys-lfu:淘汰最少用的(4.0+,更精准)
  • volatile-lru:只淘汰设了 TTL 的
  • random:随机淘汰

缓存场景推荐 allkeys-lfu(保留热点数据)。

Redis 和 MySQL 怎么保持数据一致?

这是经典难题,没有完美方案,只有最终一致

  • Cache Aside(最常用):读时先查缓存没有再查 DB 回填;写时先更新 DB 再删缓存
  • 延迟双删:先删缓存 → 改 DB → sleep 500ms → 再删缓存(防并发读到旧值回填)
  • MQ 异步删:改 DB 后发 MQ 消息,消费者删缓存,失败重试(最可靠)
  • 订阅 binlog:用 Canal 订阅 MySQL binlog,自动删缓存(解耦)

核心交易数据(如余额)别缓存或用极短 TTL。详见缓存专题

🔴 Redis 能力与生产实战 · 全栈资料库

← 返回场景决策 · 缓存专题