🎯 课程目标

从 Redis 零基础到能独立设计缓存方案、排行榜、限流器、分布式锁等高频后端场景。

👥 适合谁

有 1 年以上 JS/TS 经验的前端工程师,想掌握服务端缓存与高性能存储。

⏱️ 学习时长

认真学完约需 2-3 周,每天 1-2 小时。完成所有练习和实战任务是关键。

📍 课程地图(20 节)

阶段内容关键产出
总览(2节)课程地图、安装与 CLIRedis 环境就绪,能执行基本命令
数据结构(5节)String / Hash / List / Set / Sorted Set掌握 5 大数据结构的 CRUD
典型场景(5节)缓存设计 / Session / 限流 / 排行榜 / 分布式锁能用 Redis 解决常见业务问题
线上问题(4节)雪崩穿透击穿 / 热点 Key / 持久化 / 内存与过期具备 Redis 生产排障能力
实战(4节)缓存+MySQL / 验证码限流 / 排行榜实战 / 命令速查完整落地项目经验

🔑 前端工程师学 Redis 的核心优势

已有优势:缓存思维

你已经在用 localStorage、memo、SWR/React Query 做前端缓存,Redis 只是把缓存搬到了服务端。

已有优势:JSON 数据结构

你每天和 Object、Array、Map、Set 打交道,Redis 的 Hash、List、Set 几乎是一一对应。

已有优势:键值对概念

localStorage.setItem(key, value) 和 Redis 的 SET key value 本质一样。

需要适应:分布式思维

前端缓存是单用户的,Redis 是共享的。并发、一致性、过期策略是新挑战。

💡 学习建议:每学一个 Redis 命令,先想"这在前端怎么做?"。localStorage.setItem → SET,Map.get → HGET,几乎都有对应。
返回总入口

🔄 前端存储 vs Redis 对比

对比项前端存储Redis
存储位置浏览器本地服务端内存
速度快(本地磁盘/内存)极快(纯内存,10万+ QPS)
数据结构只能存字符串String/Hash/List/Set/ZSet 等
过期策略无内置(需手动实现)原生 TTL 支持
共享范围单浏览器单域名所有服务端进程共享
容量约 5-10 MB数十 GB(受服务器内存限制)
调试工具DevTools → Applicationredis-cli

🚀 安装步骤

# macOS(推荐 Homebrew) brew install redis brew services start redis # 后台启动 # Linux(Ubuntu/Debian) sudo apt update && sudo apt install redis-server sudo systemctl start redis-server # Docker(跨平台推荐) docker run -d --name redis -p 6379:6379 redis:7-alpine # 验证安装 redis-cli PING # 返回 PONG 即成功

🖥️ redis-cli 基础操作

# 进入交互模式(类似浏览器 Console) redis-cli # 基础键值操作 —— 类比 localStorage SET name "Alice" # localStorage.setItem("name", "Alice") GET name # localStorage.getItem("name") → "Alice" DEL name # localStorage.removeItem("name") EXISTS name # name in localStorage(0 或 1) # 设置过期时间(前端没有的能力!) SET token "abc123" EX 3600 # 1小时后自动删除 TTL token # 查看剩余秒数 # 查看所有 Key(生产环境慎用!) KEYS * # 类比 Object.keys(localStorage) DBSIZE # 当前数据库 Key 总数

📊 redis-cli 实用技巧

# 不进入交互模式,直接执行命令 redis-cli SET greeting "Hello" redis-cli GET greeting # 查看 Key 的类型 TYPE name # 返回 string / hash / list / set / zset # 监控实时命令(调试利器) redis-cli MONITOR # 实时显示所有执行的命令 # 查看 Redis 服务信息 INFO server # 版本、端口、运行时间 INFO memory # 内存使用情况
⚠️ 生产安全提醒:① 永远不要在生产环境执行 KEYS *(会阻塞服务),用 SCAN 替代 ② FLUSHDB / FLUSHALL 会清空所有数据 ③ 默认 Redis 无密码,部署前必须配置 requirepass

📋 速查:CLI 常用命令

操作Redis 命令前端类比
写入SET key valuelocalStorage.setItem
读取GET keylocalStorage.getItem
删除DEL keylocalStorage.removeItem
判断存在EXISTS keykey in localStorage
设置过期EXPIRE key seconds无原生支持
查看类型TYPE keytypeof value
✏️ 填空:CLI 基础命令
写入键值对: name "Alice" 读取值: name 设置 60 秒过期:SET token "abc" 60
🧠 小测验:Redis 基础

以下哪个说法是正确的?

🔄 前端存储 vs Redis String 对比

能力localStorage (JS)Redis String
存储类型只能存字符串字符串、数字、二进制、JSON
最大容量约 5MB(整个域名)单个 Key 最大 512MB
原子计数需手动 get→parse→+1→setINCR 一条命令搞定
过期时间不支持EX / PX / EXAT
条件写入不支持SET key val NX
批量操作需循环MSET / MGET

📌 核心命令

# ===== 基础读写 ===== SET user:1:name "Alice" # 写入 GET user:1:name # → "Alice" DEL user:1:name # 删除 # ===== 带过期的写入(缓存核心!) ===== SET cache:home "<html>..." EX 300 # 缓存 5 分钟 SETEX session:xyz 1800 "user:1" # 30 分钟 # ===== 条件写入(分布式锁基础) ===== SET lock:order "holder1" NX EX 10 # 不存在才写 # ===== 批量操作 ===== MSET k1 "v1" k2 "v2" k3 "v3" MGET k1 k2 k3 # → ["v1", "v2", "v3"]

🔢 数字操作(计数器场景)

SET page:views 0 INCR page:views # 1(原子 +1,并发安全!) INCRBY page:views 10 # 11 DECR page:views # 10 DECRBY page:views 5 # 5 # Redis INCR 是原子操作,1000 并发也不丢数 # 前端 localStorage 手动 +1 是并发不安全的 SET price 9.99 INCRBYFLOAT price 0.01 # "10.00"
💡 Key 命名规范:用冒号分隔命名空间:业务:对象:ID:字段,如 user:1:namecache:api:/users

📋 速查:String 命令

命令说明复杂度
SET / GET / DEL写/读/删O(1)
MSET / MGET批量读写O(N)
INCR / DECR原子 ±1O(1)
INCRBY / DECRBY原子 ±NO(1)
SETEX key sec val写入+过期O(1)
SETNX key val不存在才写O(1)
✏️ 填空:String 命令
原子递增: page:views 批量读取: k1 k2 k3 不存在才写:SET lock
🧠 小测验:String 类型

关于 INCR 命令,哪个说法正确?

🔄 JS Object vs Redis Hash

操作JavaScript ObjectRedis Hash
设置字段obj.name = "Alice"HSET user:1 name "Alice"
读取字段obj.nameHGET user:1 name
读取全部Object.entries(obj)HGETALL user:1
删除字段delete obj.nameHDEL user:1 name
判断存在"name" in objHEXISTS user:1 name
字段递增手动 +1HINCRBY user:1 age 1

📌 核心命令

# ===== 写入(类比 JS Object) ===== HSET user:1 name "Alice" age "28" city "Beijing" # ===== 读取 ===== HGET user:1 name # → "Alice" HMGET user:1 name age # → ["Alice", "28"] HGETALL user:1 # → 全部字段和值 # ===== 修改 & 删除 ===== HSET user:1 city "Shanghai" # 更新 HINCRBY user:1 age 1 # 原子 +1 HDEL user:1 city # 删除字段 HEXISTS user:1 name # → 1 HLEN user:1 # → 2

💡 Hash vs String 存对象

# 方案 A:多个 String(3 个 Key,3 次请求) SET user:1:name "Alice" SET user:1:age "28" # 方案 B:Hash(1 个 Key,1 次请求)✅ 推荐 HSET user:1 name "Alice" age "28" city "Beijing" # 方案 C:String 存 JSON(更新单字段需全量读写) SET user:1 '{"name":"Alice","age":28}'
⚠️ 注意:① Hash 字段值只能是字符串 ② HGETALL 字段多时慢,优先 HMGET ③ 不能对单个字段设过期

📋 速查:Hash 命令

命令说明JS 类比
HSET key f v设置字段obj[f]=v
HGET key f读字段obj[f]
HGETALL key读全部Object.entries
HDEL key f删字段delete obj[f]
HINCRBY key f n字段 ±Nobj[f]+=n
✏️ 填空:Hash 命令
设置字段: user:1 name "Alice" 读取全部: user:1 字段加 1: user:1 age 1
🧠 小测验:Hash

存用户信息哪种方案最优?

🔄 JS Array vs Redis List

操作JS ArrayRedis List
右端追加arr.push()RPUSH key val
左端追加arr.unshift()LPUSH key val
右端弹出arr.pop()RPOP key
左端弹出arr.shift()LPOP key
切片arr.slice(0,9)LRANGE key 0 9
长度arr.lengthLLEN key
阻塞弹出不支持BLPOP key timeout

📌 核心命令

# ===== 队列 FIFO:RPUSH + LPOP ===== RPUSH queue:tasks "task1" "task2" "task3" LPOP queue:tasks # → "task1" # ===== 栈 LIFO:LPUSH + LPOP ===== LPUSH stack:undo "a1" "a2" "a3" LPOP stack:undo # → "a3" # ===== 最新动态列表 ===== LPUSH feed:user1 "发布文章A" LPUSH feed:user1 "点赞文章B" LRANGE feed:user1 0 9 # 最新 10 条 LTRIM feed:user1 0 99 # 只保留 100 条

🔒 阻塞队列

# BLPOP:没有数据时阻塞等待(最多 30 秒) BLPOP queue:orders 30 # → 有数据立刻返回,超时返回 nil # 另一个客户端生产数据 RPUSH queue:orders '{"orderId":"1001"}'
💡 LTRIM 控长度:LPUSH 后接 LTRIM 实现"只保留最新 N 条",适合浏览记录、通知列表。
⚠️ 性能:LINDEX 是 O(N),只有两端操作是 O(1)。需要随机访问请用 Sorted Set。

📋 速查:List 命令

命令说明复杂度
LPUSH/RPUSH左/右入O(1)
LPOP/RPOP左/右出O(1)
LRANGE key s e区间读O(N)
LLEN key长度O(1)
LTRIM key s e裁剪O(N)
BLPOP key t阻塞出O(1)
✏️ 填空:List 命令
右端入队: queue "task1" 左端弹出: queue 前 10 条: feed 0 9
🧠 小测验:List

实现 FIFO 队列应组合?

🔄 JS Set vs Redis Set

操作JS SetRedis Set
添加set.add(v)SADD key v
删除set.delete(v)SREM key v
包含set.has(v)SISMEMBER key v
全部[...set]SMEMBERS key
数量set.sizeSCARD key
交集手动 filterSINTER k1 k2
并集手动 spreadSUNION k1 k2
差集手动 filterSDIFF k1 k2

📌 核心命令

# ===== 基础操作 ===== SADD tags:article:1 "redis" "cache" "backend" SISMEMBER tags:article:1 "redis" # → 1 SMEMBERS tags:article:1 # → 全部 SCARD tags:article:1 # → 3 SREM tags:article:1 "backend" # 删除 # ===== 集合运算(共同好友) ===== SADD friends:alice "bob" "charlie" "david" SADD friends:bob "alice" "charlie" "eve" SINTER friends:alice friends:bob # → ["charlie"] SUNION friends:alice friends:bob # → 全部合并 SDIFF friends:alice friends:bob # → ["bob","david"]

💡 实战:点赞 + 抽奖

# 点赞 SADD likes:post:100 "user:1" "user:2" "user:3" SREM likes:post:100 "user:2" # 取消点赞 SISMEMBER likes:post:100 "user:1" # 是否点赞 SCARD likes:post:100 # 点赞数 # 抽奖 SRANDMEMBER likes:post:100 1 # 随机 1 人(不移除) SPOP likes:post:100 # 随机弹出(移除)
💡 去重统计:SADD uv:2024-01-01 "user:1",SCARD 即日 UV。

📋 速查:Set 命令

命令说明复杂度
SADD/SREM增/删O(1)
SISMEMBER判断O(1)
SCARD数量O(1)
SINTER/SUNION/SDIFF集合运算O(N*M)
SRANDMEMBER随机取O(N)
✏️ 填空:Set 命令
添加元素: tags "redis" 判断存在: tags "redis" 求交集: s1 s2
🧠 小测验:Set

SINTER 返回什么?

🔄 前端排序 vs Redis ZSet

操作前端实现Redis Sorted Set
添加带分数arr.push({k,v}); arr.sort()ZADD key score member
查排名arr.findIndex()ZRANK key member
查分数map.get(k)ZSCORE key member
Top Narr.sort().slice(0,N)ZREVRANGE key 0 N-1
范围查询arr.filter()ZRANGEBYSCORE key min max
加分find→update→sortZINCRBY key delta member

📌 核心命令

# ===== 添加成员(分数+成员) ===== ZADD leaderboard 100 "alice" 85 "bob" 92 "charlie" # ===== 查询 ===== ZSCORE leaderboard "alice" # → "100" ZRANK leaderboard "bob" # → 0(从低到高排名) ZREVRANK leaderboard "alice" # → 0(从高到低排名) # ===== 排行榜(从高到低) ===== ZREVRANGE leaderboard 0 2 WITHSCORES # → ["alice","100","charlie","92","bob","85"] # ===== 加分 ===== ZINCRBY leaderboard 20 "bob" # bob: 85→105 ZREVRANGE leaderboard 0 0 # → ["bob"](bob 变第一) # ===== 按分数范围查 ===== ZRANGEBYSCORE leaderboard 80 100 # 分数在 80-100 的 ZCOUNT leaderboard 80 100 # 数量 # ===== 删除 ===== ZREM leaderboard "charlie" ZCARD leaderboard # 成员总数

💡 实战:游戏排行榜

# 玩家得分更新(每次加分自动重新排序) ZINCRBY game:rank 50 "player:1001" ZINCRBY game:rank 30 "player:1002" ZINCRBY game:rank 80 "player:1003" # Top 10 排行榜 ZREVRANGE game:rank 0 9 WITHSCORES # 查我的排名(从 0 开始,+1 得到名次) ZREVRANK game:rank "player:1001" # → 1(第 2 名) # 我周围的人(前后各 2 名) # 先查排名,再 ZREVRANGE rank-2 rank+2
⚠️ 注意:① ZRANK 从低到高,ZREVRANK 从高到低 ② 分数相同时按成员字典序排列 ③ 从 Redis 6.2 起,部分旧命令可用 ZRANGE ... REV 等新写法替代;不同版本请以官方文档为准

📋 速查:Sorted Set 命令

命令说明复杂度
ZADD key score m添加O(logN)
ZSCORE key m查分数O(1)
ZRANK/ZREVRANK查排名O(logN)
ZREVRANGE key s eTop NO(logN+M)
ZINCRBY key d m加分O(logN)
ZCARD key总数O(1)
✏️ 填空:Sorted Set
添加: rank 100 "alice" Top 3(高→低): rank 0 2 加分: rank 10 "alice"
🧠 小测验:Sorted Set

ZREVRANK 返回的排名方向是?

🔄 前端缓存 vs Redis 缓存

维度前端缓存Redis 缓存
工具React Query / SWRRedis + 应用代码
位置浏览器内存服务端内存
过期staleTimeTTL(EX / PX)
更新invalidate旁路更新 / 删除
共享单用户所有用户所有请求

📌 Cache-Aside 旁路缓存

# 读流程: # 1. 先查 Redis GET cache:user:1 # 命中 → 返回 | 未命中 ↓ # 2. 查数据库 → SELECT * FROM users WHERE id = 1 # 3. 回填 Redis SET cache:user:1 '{"name":"Alice"}' EX 3600 # 写流程: # 1. 更新 DB → 2. 删缓存(非更新!) DEL cache:user:1

🔑 缓存更新策略

# Cache-Aside ✅ 推荐:先 DB → 删缓存 # Write-Through:同步双写(一致性好但慢) # Write-Behind:先 Redis,异步 DB(快但可能丢) # Refresh-Ahead:快过期时刷新(类似 SWR)
💡 删缓存 vs 更新缓存?并发时更新缓存可能写入旧值,删除更安全。
⚠️ 删缓存失败?① 消息队列重试 ② 短 TTL 兜底 ③ Canal 订阅 binlog

📋 速查:缓存模式

模式适用
Cache-Aside先缓存后DB先DB后删缓存通用
Write-Through从缓存读同步双写强一致
Write-Behind从缓存读异步回写高写入
✏️ 填空:缓存模式
读:先查 ,miss 查 DB,回填 写:先更新 ,再 缓存
🧠 小测验:缓存设计

Cache-Aside 数据更新时应该?

🔄 前端登录态 vs Redis Session

维度前端方案Redis Session
存储Cookie / sessionStorageRedis 服务端
安全客户端可篡改服务端控制
容量Cookie 4KB无限制
多服务器无关共享
强制下线需前端配合DEL 即可

📌 Redis Session 实现

# 1. 登录 → 创建 Session HSET sess:abc123 userId "1001" username "alice" role "admin" EXPIRE sess:abc123 1800 # 30 分钟 # 2. 请求验证 HGETALL sess:abc123 EXPIRE sess:abc123 1800 # 续期 # 3. 登出 / 强制下线 DEL sess:abc123

💡 Hash vs String 存 Session

# String:需反序列化整个 JSON SET sess:abc '{"userId":"1001","role":"admin"}' # Hash(推荐):可单独读写字段 HSET sess:abc userId "1001" role "admin" HGET sess:abc role # 只读 role
💡 续期:每次请求 EXPIRE 刷新 TTL,活跃用户不过期。
⚠️ 安全:① sessionId 用 UUID v4 ② HTTPS Only Cookie ③ 合理 TTL

📋 速查:Session 命令

操作命令
创建HSET sess:id f v + EXPIRE
读取HGETALL sess:id
续期EXPIRE sess:id 1800
登出DEL sess:id
✏️ 填空:Session
创建: sess:abc userId "1001" 过期: sess:abc 1800 登出: sess:abc
🧠 小测验:Session

Redis Session 最大优势?

🔄 前端节流 vs 服务端限流

维度前端 throttleRedis 限流
位置浏览器服务端
可靠性用户可绕过无法绕过
粒度按函数调用按用户/IP/接口
策略固定间隔固定窗口/滑动窗口/令牌桶
存储闭包变量Redis Key + TTL

📌 固定窗口限流

# 限制:每个 IP 每分钟最多 60 次请求 # Key 格式:rate:IP:当前分钟 INCR rate:192.168.1.1:202401011030 # 第一次调用自动创建 Key,值为 1 EXPIRE rate:192.168.1.1:202401011030 60 # 设置 60 秒过期 # 判断是否超限 GET rate:192.168.1.1:202401011030 # 值 > 60 → 返回 429 Too Many Requests

📌 滑动窗口限流(更精确)

# 用 Sorted Set 实现滑动窗口 # Key:rate:userId Score:时间戳 Member:请求ID # 1. 记录请求 ZADD rate:user:1001 1704067200 "req:uuid1" # 2. 移除窗口外的旧请求(60秒前) ZREMRANGEBYSCORE rate:user:1001 0 1704067140 # 3. 统计窗口内请求数 ZCARD rate:user:1001 # > 60 → 拒绝 # 4. 设置 Key 过期(兜底清理) EXPIRE rate:user:1001 61
💡 Lua 脚本保原子性:生产环境应将 INCR+EXPIRE 或 ZADD+ZREMRANGEBYSCORE+ZCARD 封装到 Lua 脚本中,确保原子执行。
⚠️ 固定窗口的问题:窗口边界时刻可能出现双倍流量(前一窗口末尾 + 后一窗口开头),滑动窗口可以解决。

📋 速查:限流方案

方案数据结构精度复杂度
固定窗口String INCR简单
滑动窗口Sorted Set中等
令牌桶Lua 脚本复杂
✏️ 填空:限流命令
固定窗口计数: rate:ip:key 设过期: rate:ip:key 60 滑动窗口删旧: rate:user 0 oldTs
🧠 小测验:限流

固定窗口限流的缺点是什么?

🔄 前端排行榜 vs Redis 排行榜

需求前端实现Redis ZSet
更新分数find→update→sortZINCRBY rank 10 "user"
Top 10sort→slice(0,10)ZREVRANGE rank 0 9
我的排名findIndexZREVRANK rank "user"
总人数arr.lengthZCARD rank
并发安全不安全原子操作

📌 完整排行榜实现

# ===== 1. 更新分数 ===== ZINCRBY game:rank 50 "player:1001" ZINCRBY game:rank 30 "player:1002" ZINCRBY game:rank 80 "player:1003" ZINCRBY game:rank 65 "player:1004" # ===== 2. 获取 Top 3 ===== ZREVRANGE game:rank 0 2 WITHSCORES # → ["player:1003","80","player:1004","65","player:1001","50"] # ===== 3. 查我的排名 ===== ZREVRANK game:rank "player:1001" # → 2(第 3 名,0 开始) ZSCORE game:rank "player:1001" # → "50" # ===== 4. 周排行榜(用 Key 区分) ===== ZINCRBY rank:week:2024-01 50 "player:1001" EXPIRE rank:week:2024-01 604800 # 7 天过期

💡 多维度排行榜

# 日榜 / 周榜 / 月榜 / 总榜 ZINCRBY rank:daily:20240101 10 "user:1" ZINCRBY rank:weekly:2024W01 10 "user:1" ZINCRBY rank:monthly:202401 10 "user:1" ZINCRBY rank:all 10 "user:1" # 合并多日榜为周榜 ZUNIONSTORE rank:weekly:2024W01 7 rank:daily:20240101 rank:daily:20240102 ... # 分数相同时如何排序? # Redis 默认按字典序,可用复合分数:score = 主分数 * 10^10 + (MAX_TS - 时间戳)
💡 ZUNIONSTORE:可以将多个 ZSet 合并,非常适合"合并日榜为周榜"这类需求。
⚠️ 性能:百万级成员的 ZREVRANGE 0 9 仍然是 O(logN+M),性能极好。但 ZRANGEBYSCORE 范围太大时要注意。

📋 速查:排行榜命令

需求命令
加分ZINCRBY rank delta member
Top NZREVRANGE rank 0 N-1 WITHSCORES
我的排名ZREVRANK rank member
我的分数ZSCORE rank member
总人数ZCARD rank
合并榜单ZUNIONSTORE dest N src1 src2
✏️ 填空:排行榜
加 10 分: rank 10 "user:1" Top 5: rank 0 4 我的排名: rank "user:1"
🧠 小测验:排行榜

ZREVRANK 返回 0 表示什么?

🔄 前端互斥 vs 分布式锁

维度前端分布式锁
场景按钮防重复点击防止超卖、重复支付
实现loading 状态 / disabledRedis SET NX
范围单用户单页面所有服务器所有进程
释放请求完成恢复按钮DEL 或 TTL 自动释放

📌 基础分布式锁

# ===== 加锁 ===== # SET key value NX EX seconds # NX:不存在才写入(互斥) # EX:设过期(防死锁) SET lock:order:1001 "server-1-uuid" NX EX 30 # 返回 OK → 加锁成功 # 返回 nil → 已被其他人持有 # ===== 释放锁(必须验证持有者!) ===== # 错误做法:直接 DEL(可能删别人的锁) DEL lock:order:1001 # ← 危险! # 正确做法:Lua 脚本原子判断+删除 # if redis.call("GET",KEYS[1]) == ARGV[1] then # return redis.call("DEL",KEYS[1]) # else return 0 end

⚠️ 分布式锁的陷阱

# 陷阱 1:锁过期但业务没完成 # → 解决:看门狗机制(Redisson 自动续期) # 陷阱 2:释放了别人的锁 # A 加锁 → A 处理超时 → 锁自动过期 → B 加锁 → A 完成 DEL → 删了 B 的锁! # → 解决:value 存 UUID,删前比对 # 陷阱 3:Redis 主从切换丢锁 # A 在主节点加锁 → 主节点宕机 → 从节点提升(没有锁) → B 也加锁成功 # → 解决:Redlock 算法(多节点加锁) # 生产建议:优先使用成熟库,如 Redisson(Java)或 node-redlock(Node.js) # 是否需要 Redlock,要看你对主从切换/多节点容错的要求
💡 类比前端:分布式锁就像按钮的 loading 状态,只是作用范围从"单用户"扩展到"全系统所有进程"。
⚠️ 核心原则:① 加锁必须设过期(防死锁) ② 释放必须验证持有者(防误删) ③ 生产环境优先用成熟库,而不是手写锁逻辑

📋 速查:分布式锁

操作命令注意
加锁SET key uuid NX EX 30NX 互斥 + EX 防死锁
释放Lua: GET→比对→DEL必须原子操作
续期EXPIRE key 30看门狗自动续
✏️ 填空:分布式锁
加锁:SET lock:order "uuid" 30 释放前必须验证 是否是自己
🧠 小测验:分布式锁

为什么加锁时必须设置 EX 过期?

🔄 三大问题对比

问题场景类比前端核心解法
缓存雪崩大量 Key 同时过期CDN 节点全部失效随机过期时间
缓存穿透查询不存在的数据请求不存在的 API布隆过滤器 / 缓存空值
缓存击穿热点 Key 过期热门页面缓存失效互斥锁 / 永不过期

❄️ 缓存雪崩

# 问题:大量 Key 设了相同过期时间,同时失效 → DB 被压垮 # 解法 1:随机过期时间 # 原来:SET cache:user:1 val EX 3600(都是 1 小时) # 改为:EX 3600 + random(0, 300)(加随机偏移) SET cache:user:1 "val" EX 3720 # 3600 + 120 SET cache:user:2 "val" EX 3850 # 3600 + 250 # 解法 2:多级缓存 # L1: 本地缓存(进程内,最快) # L2: Redis(共享缓存) # L3: 数据库 # 解法 3:热点数据预加载 + 永不过期

🕳️ 缓存穿透

# 问题:查 id=-1 的用户,Redis 没有,DB 也没有 # → 每次都穿透到 DB,恶意攻击可打垮 DB # 解法 1:缓存空值 GET cache:user:-1 # miss # 查 DB → 不存在 → 缓存空值 SET cache:user:-1 "NULL" EX 60 # 短过期 # 解法 2:布隆过滤器(推荐) # 把所有合法 ID 加入布隆过滤器 # 请求来时先查布隆过滤器 → 不存在则直接拒绝 # BF.ADD users_filter "1" # BF.EXISTS users_filter "-1" → 0(一定不存在) # 解法 3:参数校验 # ID 为负数直接返回空

🔥 缓存击穿

# 问题:某热点 Key 过期,大量并发同时查 DB # 解法 1:互斥锁(用分布式锁) GET cache:hot-product # miss → 加锁 SET lock:rebuild:hot-product "1" NX EX 10 # 加锁成功 → 查 DB → 回填缓存 → 释放锁 # 加锁失败 → 短暂等待 → 重试 GET # 解法 2:逻辑过期(永不过期 + 异步更新) # 缓存值:{"data":"...","expireAt":1704067200} # Key 本身不设 TTL,由应用逻辑判断是否过期 # 过期 → 开后台线程更新,当前返回旧数据
⚠️ 面试必问:三大问题的区别——雪崩是"大面积"失效,穿透是"查不存在的",击穿是"单个热点"失效。

📋 速查:缓存问题与解法

问题解法
雪崩随机 TTL / 多级缓存 / 预加载
穿透布隆过滤器 / 缓存空值 / 参数校验
击穿互斥锁 / 逻辑过期 / 永不过期
✏️ 填空:缓存问题
大量 Key 同时过期叫缓存 查不存在的数据叫缓存 热点 Key 过期叫缓存
🧠 小测验:缓存问题

防止缓存穿透的最佳方案是?

🔄 前端热点请求 vs Redis 热 Key

维度前端Redis 热 Key
场景热门商品页疯狂刷新明星微博、秒杀商品
问题API 被打挂单个 Redis 节点过载
解法CDN / 本地缓存本地缓存 / 读副本 / 拆 Key

📌 发现热点 Key

# 方法 1:redis-cli --hotkeys(Redis 4.0+) redis-cli --hotkeys # 方法 2:MONITOR 实时监控(短时间!) redis-cli MONITOR | head -1000 # 方法 3:业务层统计 # 在代码中对 Redis 请求计数,超阈值告警 # 方法 4:Redis 慢查询日志 SLOWLOG GET 10

📌 解决热点 Key

# 解法 1:本地缓存(L1 缓存) # 应用进程内存缓存热数据,不走 Redis # Node.js 示例: # const cache = new Map(); # if (cache.has(key)) return cache.get(key); # const val = await redis.get(key); # cache.set(key, val); setTimeout(() => cache.delete(key), 1000); # 解法 2:读副本(Redis Cluster 读写分离) # 读请求分散到多个从节点 # 解法 3:拆 Key(分片) # 原来:GET hot:product:1 # 拆成:GET hot:product:1:{random(0,9)} # 10 个 Key 分散压力,读时随机选一个
💡 类比 CDN:热点 Key 的本地缓存就像前端用 CDN 缓存静态资源,把压力分散到边缘节点。
⚠️ Big Key 也是问题:单个 Value 超过 10KB 就算 Big Key,会导致网络带宽和序列化瓶颈。用 MEMORY USAGE key 检查。

📋 速查:热 Key 处理

方案原理适用
本地缓存进程内 Map读多写少
读副本主从分离集群环境
拆 Key分散到多 Key极端热点
✏️ 填空:热 Key
发现热 Key:redis-cli 拆 Key 分散压力:GET hot:product:1: 检查 Value 大小: key
🧠 小测验:热 Key

热点 Key 本地缓存类似前端的什么?

🔄 前端存储持久化 vs Redis 持久化

维度前端Redis
内存数据JS 变量(刷新就丢)Redis 数据(重启就丢)
持久化localStorage / IndexedDBRDB / AOF
快照JSON.stringify 整体存RDB(全量快照)
日志记录每次 setStateAOF(记录每条写命令)

📸 RDB(快照)

# RDB:某个时间点的全量快照 → dump.rdb 文件 # 类比:每隔一段时间 JSON.stringify(state) 存到磁盘 # 触发方式 SAVE # 同步保存(阻塞!生产慎用) BGSAVE # 后台异步保存(推荐) # 自动触发配置(redis.conf) # save 900 1 # 900秒内至少1次写入 → 触发 BGSAVE # save 300 10 # 300秒内至少10次写入 # save 60 10000 # 60秒内至少10000次写入 # 优点:文件小、恢复快、适合备份 # 缺点:可能丢失最后一次快照后的数据

📝 AOF(追加日志)

# AOF:记录每条写命令 → appendonly.aof 文件 # 类比:记录每次 dispatch(action),可以重放还原状态 # 开启 AOF(redis.conf) # appendonly yes # 同步策略 # appendfsync always # 每条命令都刷盘(最安全,最慢) # appendfsync everysec # 每秒刷盘(推荐,最多丢 1 秒) # appendfsync no # OS 决定(最快,可能丢数据) # AOF 重写(压缩日志) BGREWRITEAOF # 后台重写,合并冗余命令 # 优点:丢数据少(最多 1 秒) # 缺点:文件大、恢复慢
💡 推荐配置:生产环境同时开启 RDB + AOF。RDB 用于定期备份,AOF 用于数据恢复(丢失更少)。Redis 4.0+ 支持混合持久化。
⚠️ 注意:① 纯缓存场景可以不开持久化 ② SAVE 会阻塞所有请求,只用 BGSAVE ③ AOF 文件需要定期重写避免过大

📋 速查:持久化对比

维度RDBAOF
原理全量快照追加写命令
文件dump.rdbappendonly.aof
丢数据最后快照后的最多 1 秒
恢复速度
文件大小
✏️ 填空:持久化
后台保存快照: 开启 AOF:appendonly 推荐同步策略:appendfsync
🧠 小测验:持久化

生产环境推荐的持久化策略?

🔄 前端内存管理 vs Redis 内存管理

维度前端 JSRedis
内存回收GC 自动回收过期删除 + 淘汰策略
内存限制浏览器 tab ~1-4GBmaxmemory 配置
内存泄漏闭包/事件监听未清理Key 无 TTL 不断累积
监控DevTools → MemoryINFO memory

⏰ 过期删除策略

# Redis 不会在 Key 过期的瞬间立刻删除! # 策略 1:惰性删除 # 访问 Key 时检查是否过期 → 过期则删除并返回 nil GET expired:key # 此时才真正删除 # 策略 2:定期删除 # Redis 每 100ms 随机抽样检查 20 个 Key # 如果超过 25% 已过期 → 继续抽样检查 # 保证不会阻塞太久 # 两种策略结合使用: # 定期删除 → 兜底清理 # 惰性删除 → 精准清理

📊 内存淘汰策略(maxmemory-policy)

# 当内存达到 maxmemory 上限时触发 # 查看当前配置 CONFIG GET maxmemory CONFIG GET maxmemory-policy # 设置内存上限 CONFIG SET maxmemory 2gb # 8 种淘汰策略: # noeviction → 不淘汰,写入报错(默认) # allkeys-lru → 所有 Key 中淘汰最近最少使用 ✅ 推荐 # allkeys-lfu → 所有 Key 中淘汰最不经常使用 # allkeys-random → 所有 Key 中随机淘汰 # volatile-lru → 有过期时间的 Key 中淘汰 LRU # volatile-lfu → 有过期时间的 Key 中淘汰 LFU # volatile-random → 有过期时间的 Key 中随机淘汰 # volatile-ttl → 有过期时间的 Key 中淘汰 TTL 最短的

🔍 内存诊断

# 查看内存使用概览 INFO memory # used_memory: 当前使用内存 # used_memory_peak: 历史最大内存 # maxmemory: 上限 # 查看单个 Key 的内存占用 MEMORY USAGE user:1 # 查看 Key 数量 DBSIZE # 查看各类型 Key 分布(抽样统计) redis-cli --bigkeys
💡 缓存场景推荐:allkeys-lru,让 Redis 自动淘汰冷数据。类比浏览器缓存满时自动清理最久没用的资源。
⚠️ 内存优化:① 大多数缓存 Key 都应设 TTL;少数永久缓存要配合明确失效策略 ② 用 Hash 代替多个 String(省内存) ③ 定期用 --bigkeys 排查大 Key

📋 速查:内存管理

命令说明
INFO memory内存使用概览
MEMORY USAGE key单 Key 内存
DBSIZEKey 总数
CONFIG SET maxmemory设内存上限
CONFIG SET maxmemory-policy设淘汰策略
✏️ 填空:内存管理
查看内存:INFO 推荐淘汰策略: 查 Key 内存占用: key
🧠 小测验:内存管理

缓存场景推荐哪种淘汰策略?

🔄 架构对比

层级前端架构后端 Redis+MySQL
L1 缓存组件 state / useMemo进程内 Map
L2 缓存React Query cacheRedis
数据源API ServerMySQL

📌 读流程(TypeScript 伪代码)

// Cache-Aside 读流程 async function getUser(userId: string) { const cacheKey = `cache:user:${userId}`; // 1. 先查 Redis const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); // 命中缓存 } // 2. 未命中 → 查 MySQL const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]); if (!user) return null; // 3. 回填 Redis(设 TTL) await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); return user; }

📌 写流程

// Cache-Aside 写流程 async function updateUser(userId: string, data: Partial<User>) { // 1. 先更新 MySQL await db.query('UPDATE users SET ? WHERE id = ?', [data, userId]); // 2. 再删除缓存(而非更新!) await redis.del(`cache:user:${userId}`); // 下次读取时自动回填最新数据 } // 批量更新时删除相关缓存 async function updateUserBatch(userIds: string[]) { // ... 批量更新 DB const keys = userIds.map(id => `cache:user:${id}`); await redis.del(...keys); // 批量删除缓存 }
💡 Redis 命令对应:代码中的 redis.get/set/del 对应 CLI 的 GET/SET/DEL,参数一一对应。
⚠️ 注意:① 删缓存失败要有重试机制 ② JSON 序列化有性能开销,大对象考虑 Hash ③ 设合理 TTL 作为兜底

📋 速查:Cache-Aside 要点

步骤操作Redis 命令
读-1查缓存GET cache:user:id
读-2查 DBSQL SELECT
读-3回填SET cache:user:id val EX 3600
写-1更新 DBSQL UPDATE
写-2删缓存DEL cache:user:id
✏️ 填空:Cache-Aside
读:先 cache:user:1 未命中查 DB 后: cache:user:1 data 3600
🧠 小测验:缓存+MySQL

写操作时为什么删缓存而不是更新缓存?

🔄 前端验证 vs 服务端验证

维度前端验证Redis 服务端验证
发送频率按钮 disabled 60sRedis INCR + TTL
存储验证码不存(安全隐患)Redis SET EX
验证次数无法限制Redis DECR
可靠性用户可绕过无法绕过

📌 完整实现(Redis 命令流)

# ===== 1. 发送验证码(频率限制) ===== # 检查 60 秒内是否已发送 EXISTS sms:lock:13800138000 # → 1 → 拒绝:"请 60 秒后再试" # → 0 → 继续 # 发送锁(60 秒内不能重发) SET sms:lock:13800138000 "1" EX 60 # 每日发送上限 INCR sms:daily:13800138000 EXPIRE sms:daily:13800138000 86400 # 一天过期 # 值 > 10 → 拒绝:"今日发送次数已达上限" # ===== 2. 存储验证码 ===== SET sms:code:13800138000 "826451" EX 300 # 5 分钟有效 SET sms:retry:13800138000 "3" EX 300 # 最多验证 3 次

📌 验证流程

# ===== 3. 用户提交验证码 ===== # 检查剩余次数 GET sms:retry:13800138000 # → "0" → 拒绝:"验证次数已用完" # → > 0 → 继续 # 读取正确验证码 GET sms:code:13800138000 # → nil → "验证码已过期" # → "826451" → 比对用户输入 # 比对成功 → 清理 DEL sms:code:13800138000 DEL sms:retry:13800138000 # 比对失败 → 扣次数 DECR sms:retry:13800138000

💡 TypeScript 实现

async function sendSmsCode(phone: string) { // 频率限制 if (await redis.exists(`sms:lock:${phone}`)) { throw new Error('请 60 秒后再试'); } const daily = await redis.incr(`sms:daily:${phone}`); await redis.expire(`sms:daily:${phone}`, 86400); if (daily > 10) throw new Error('今日次数已达上限'); const code = Math.random().toString().slice(-6); await redis.set(`sms:code:${phone}`, code, 'EX', 300); await redis.set(`sms:retry:${phone}`, '3', 'EX', 300); await redis.set(`sms:lock:${phone}`, '1', 'EX', 60); // ... 调用短信 API 发送 code }
💡 Key 命名:sms:lock(发送锁)、sms:code(验证码)、sms:retry(重试次数)、sms:daily(日限额)——命名清晰是运维的第一步。
⚠️ 安全:① 验证码不要返回给前端 ② 用 Lua 脚本保证原子性 ③ 日限额防止短信轰炸

📋 速查:验证码相关 Key

Key用途TTL
sms:lock:phone发送频率锁60s
sms:code:phone验证码300s
sms:retry:phone剩余次数300s
sms:daily:phone日发送数86400s
✏️ 填空:验证码限流
存验证码 5 分钟: sms:code:phone "826451" EX 验证失败扣次数: sms:retry:phone
🧠 小测验:验证码限流

sms:lock:phone 的作用是?

🔄 前端排序实现 vs Redis ZSet

功能前端实现Redis ZSet
性能O(NlogN) 每次排序O(logN) 插入自动有序
并发不安全ZINCRBY 原子操作
百万级浏览器容易卡顿通常仍能保持较低延迟(取决于命令范围和数据分布)
持久化刷新就丢服务端持久存储

📌 完整排行榜 API(Redis 命令)

# ===== 1. 更新积分 ===== ZINCRBY rank:game:2024 100 "player:alice" ZINCRBY rank:game:2024 85 "player:bob" ZINCRBY rank:game:2024 92 "player:charlie" # ===== 2. 获取 Top 10(带分数) ===== ZREVRANGE rank:game:2024 0 9 WITHSCORES # ===== 3. 我的排名和分数 ===== ZREVRANK rank:game:2024 "player:alice" # → 0(第 1 名) ZSCORE rank:game:2024 "player:alice" # → "100" # ===== 4. 排名附近的人(前后各 2 名) ===== # 先查排名 ZREVRANK rank:game:2024 "player:bob" # → 2 # 再取范围 [2-2, 2+2] = [0, 4] ZREVRANGE rank:game:2024 0 4 WITHSCORES # ===== 5. 总参与人数 ===== ZCARD rank:game:2024

📌 多维度排行榜

# 日榜 ZINCRBY rank:daily:20240101 100 "player:alice" EXPIRE rank:daily:20240101 172800 # 2 天后清理 # 周榜(合并 7 个日榜) ZUNIONSTORE rank:weekly:2024W01 7 \ rank:daily:20240101 rank:daily:20240102 \ rank:daily:20240103 rank:daily:20240104 \ rank:daily:20240105 rank:daily:20240106 \ rank:daily:20240107 EXPIRE rank:weekly:2024W01 604800 # 分数相同按时间排序(先达到的排前面) # 复合分数 = score * 10^13 + (MAX_TS - timestamp) # ZADD rank 100_9999999999999 "alice"

💡 TypeScript API 实现

class LeaderboardService { private key = 'rank:game:2024'; async addScore(player: string, score: number) { return redis.zincrby(this.key, score, player); } async getTopN(n: number) { return redis.zrevrange(this.key, 0, n - 1, 'WITHSCORES'); } async getMyRank(player: string) { const rank = await redis.zrevrank(this.key, player); const score = await redis.zscore(this.key, player); return { rank: rank !== null ? rank + 1 : null, score }; } async getNearby(player: string, range = 2) { const rank = await redis.zrevrank(this.key, player); if (rank === null) return []; const start = Math.max(0, rank - range); return redis.zrevrange(this.key, start, rank + range, 'WITHSCORES'); } }
💡 ZUNIONSTORE:可以将多个日榜合并成周榜/月榜,还支持 WEIGHTS 参数设置权重。
⚠️ 注意:① ZREVRANK 从 0 开始,展示给用户要 +1 ② 排行榜数据要设 EXPIRE 防止无限增长 ③ 百万级数据的 ZREVRANGE 0 9 性能仍然很好

📋 速查:排行榜 API

功能Redis 命令
加分ZINCRBY rank score player
Top NZREVRANGE rank 0 N-1 WITHSCORES
我的排名ZREVRANK rank player
我的分数ZSCORE rank player
合并榜单ZUNIONSTORE dest N src1 src2
总人数ZCARD rank
✏️ 填空:排行榜实战
加分: rank 50 "alice" Top 10:ZREVRANGE rank WITHSCORES
🧠 小测验:排行榜实战

ZUNIONSTORE 的作用?

🔤 String 命令

命令说明示例前端类比
SET key val写入SET name "Alice"localStorage.setItem
GET key读取GET namelocalStorage.getItem
DEL key删除DEL namelocalStorage.removeItem
SET k v EX s写入+过期SET token "x" EX 3600无原生支持
INCR key原子 +1INCR views无原子操作
MSET / MGET批量读写MSET k1 v1 k2 v2循环 setItem
SET k v NX不存在才写SET lock "1" NX EX 30不支持

🗂️ Hash 命令

命令说明示例前端类比
HSET key f v设置字段HSET user:1 name "Alice"obj.name = "Alice"
HGET key f读取字段HGET user:1 nameobj.name
HGETALL key全部字段HGETALL user:1Object.entries(obj)
HDEL key f删除字段HDEL user:1 namedelete obj.name
HINCRBY key f n字段递增HINCRBY user:1 age 1obj.age += 1

📋 List 命令

命令说明示例前端类比
RPUSH key val右端入RPUSH queue "task"arr.push()
LPUSH key val左端入LPUSH stack "item"arr.unshift()
LPOP / RPOP弹出LPOP queuearr.shift()
LRANGE key s e区间读LRANGE feed 0 9arr.slice(0,10)
LLEN key长度LLEN queuearr.length

🔵 Set 命令

命令说明示例前端类比
SADD key val添加SADD tags "redis"set.add()
SISMEMBER key v判断SISMEMBER tags "redis"set.has()
SMEMBERS key全部SMEMBERS tags[...set]
SINTER k1 k2交集SINTER friends:a friends:b手动 filter
SCARD key数量SCARD tagsset.size

🏆 Sorted Set 命令

命令说明示例
ZADD key score m添加ZADD rank 100 "alice"
ZINCRBY key d m加分ZINCRBY rank 10 "alice"
ZREVRANGE key s eTop NZREVRANGE rank 0 9 WITHSCORES
ZREVRANK key m排名ZREVRANK rank "alice"
ZSCORE key m分数ZSCORE rank "alice"
ZCARD key总数ZCARD rank

⚙️ 通用命令

命令说明示例
EXPIRE key s设过期EXPIRE sess:abc 1800
TTL key剩余秒数TTL sess:abc
EXISTS key是否存在EXISTS user:1
TYPE key类型TYPE user:1
SCAN cursor遍历 KeySCAN 0 MATCH user:*
INFO section服务器信息INFO memory
MEMORY USAGE keyKey 内存MEMORY USAGE user:1

🎯 场景速查

场景数据结构核心命令
缓存StringSET k v EX ttl / GET / DEL
SessionHashHSET / HGETALL / EXPIRE / DEL
计数器StringINCR / INCRBY / DECR
限流String/ZSetINCR+EXPIRE / ZADD+ZCARD
排行榜Sorted SetZINCRBY / ZREVRANGE / ZREVRANK
分布式锁StringSET k v NX EX + Lua DEL
消息队列ListRPUSH / BLPOP
标签/去重SetSADD / SISMEMBER / SINTER
点赞SetSADD / SREM / SCARD
最近动态ListLPUSH / LRANGE / LTRIM
💡 恭喜完成所有课程!你已经掌握了 Redis 的 5 大数据结构、5 大典型场景、4 大线上问题和 4 个完整实战项目。下一步建议:在真实项目中实践,遇到问题回来查这个速查表。
✏️ 终极填空:场景选型
排行榜用 数据结构 Session 用 数据结构 分布式锁用 SET key val EX
🧠 终极测验:场景选型

实现"共同好友"功能应该用哪个数据结构?