Redis 速查手册
Redis 面试应用手册(C++ 后端实习版)
面向大厂 C++ 后端实习面试。不讲底层源码原理,只讲三件事: ① 这个功能实际怎么用(命令 + 场景 + 规范) ② 面试会怎么问(原题 + 标准答案 + 话术) ③ 追问怎么接(面试官的第二、三问)
使用建议:按章过一遍 → 考前只背第 8 章速查表。
目录
- 1. 全景与定位
- 2. 五大基本类型:用法与场景
- 3. 其他类型:Bitmap / Bitfield / HyperLogLog / GEO / Stream / Pub-Sub
- 4. 缓存三大问题与一致性(必考)
- 5. 分布式锁(必考)
- 6. 事务 / 管道 / Lua
- 7. 持久化与高可用:怎么选
- 8. 生产规范与常见坑
- 9. 面试题速查(考前背这个)
- 10. 附录:命令速查
1. 全景与定位
1.1 它是什么、什么时候用
一句话:基于内存的 KV 数据库,主要用作缓存,也可做分布式锁、消息队列、计数器、排行榜。
你项目里什么时候该用它:
| 场景 | 用 Redis 的理由 |
|---|---|
| 热点数据读取太慢 | 内存读,比查 MySQL 快几个数量级 |
| 多台服务器要共享状态 | 如登录态、在线状态 |
| 需要跨机器互斥 | 分布式锁 |
| 需要计数器/排行榜 | INCR、ZSet 天然支持 |
| 需要临时数据自动过期 | TTL 机制 |
什么时候不该用:
- 强一致要求——如扣款(要用关系型数据库的事务)。
- 复杂查询——如多条件聚合、join(Redis 做不了)。
- 数据量超过内存——成本太高,考虑其他方案。
- 需要持久化的核心数据——Redis 持久化有丢数据风险,只能当缓存或可重建的数据。
1.2 面试怎么答「Redis 是什么」
标准话术「Redis 是一个基于内存的 KV 数据库,主要用作缓存,也支持分布式锁、消息队列这些场景。它的数据类型比 Memcached 丰富,支持持久化,还有原生集群。我一般用它做缓存、计数器、分布式锁。」
面试题:Redis 和 Memcached 的区别?
标准答案(三条足够) 「① 数据结构:Redis 支持 5 种以上,Memcached 只有 String;② 持久化:Redis 支持 RDB/AOF,Memcached 不支持;③ 集群:Redis 有原生 Cluster,Memcached 要客户端分片。所以现在基本都用 Redis。」
面试题:Redis 为什么快?
标准话术(别只说单线程) 「三个原因:① 基于内存,读写不碰磁盘;② 单线程处理命令,没有锁竞争和上下文切换的开销;③ IO 多路复用,一个线程用 epoll 管理海量连接。另外它的数据结构都是为场景定制的,效率高。 补充一点:6.0 之后网络 IO 是多线程的,但命令执行还是单线程,所以严格说不算纯单线程了。」
追问:为什么用单线程不用多线程?
「因为 Redis 的瓶颈是网络 IO 和内存,不是 CPU。多线程并行收益有限,反而会引入锁竞争和并发 bug。所以单线程 + epoll 是性价比最高的选择。」
1.3 单线程带来的实际影响(应用层,必须知道)
因为单线程,一条慢命令会阻塞所有请求。所以:
| 危险操作 | 为什么危险 | 正确做法 |
|---|---|---|
KEYS * | 遍历所有 key,O(N) | 用 SCAN 渐进遍历 |
FLUSHALL / FLUSHDB | 同步删除大量数据 | 加 ASYNC 异步执行 |
DEL 大 key | 同步释放大量内存会卡 | 用 UNLINK 异步删除 |
HGETALL / SMEMBERS 大集合 | 一次返太多数据 | 用 HSCAN / SSCAN 分批 |
LRANGE key 0 -1 大列表 | 同上 | 分页取 |
面试怎么答:「因为 Redis 命令执行是单线程的,所以要避免慢命令阻塞。生产上不能用
KEYS *,要用SCAN;删大 key 要用UNLINK不用DEL;批量取数据要分批。」
面试题:Redis 大 key 有什么问题?怎么处理?
标准答案「大 key 一般指 String 超 10KB,或者集合类元素超 5000 个。问题有三个:网络传输慢、删除时阻塞(同步释放内存)、集群下数据倾斜(都落在一个分片)。 处理:① 排查用
redis-cli --bigkeys或MEMORY USAGE key;② 拆分(一个 List 拆成多个)、压缩;③ 删除用UNLINK异步删。」
面试题:热 key 是什么?怎么解决?
「热 key 是某个 key 访问量特别大,导致单个节点压力过大(Cluster 下就是单个分片被打爆)。 解决:① 本地缓存——在应用进程里加一层 Caffeine 缓存,减少 Redis 访问;② 分散 key——把一个热 key 复制成多份(
key_1、key_2…),请求随机选一个读。 排查用redis-cli --hotkeys。」
2. 五大基本类型:用法与场景
每种给三样:命令 / 典型场景 / 面试题。
2.1 String
命令
SET k v # 设置
GET k # 获取
SETEX k 10 v # 设值 + 10秒过期
SETNX k v # 不存在才设置
SET k v NX PX 10000 # 原子设置(NX=不存在才设,PX=毫秒过期)
INCR k / DECR k # 原子自增/自减
MSET/MGET # 批量
典型场景
| 场景 | 用法 |
|---|---|
| 缓存对象 | 对象转 JSON 字符串存 |
| 计数器 | INCR 文章阅读量、点赞数(原子,并发不会算错) |
| 分布式锁 | SET k v NX PX 30000 |
| 限流 | INCR + EXPIRE 做简单计数器限流 |
| Session 共享 | 多服务器共享登录态 |
面试题:缓存对象用 String 还是 Hash?
标准答案「看情况。如果只整体读写,用 String 存 JSON 更简单,一次读写搞定。 如果需要单独修改某个字段,用 Hash 更好——比如用户信息,改个年龄不用把整个对象读出来改了再写回。 另外 Hash 的内存占用通常比多个 String 小。」
面试题:String 底层是什么?为什么不用 C 字符串?
标准话术(一句话带过原理即可) 「底层是 SDS,不用 C 字符串主要是三点:① SDS 有长度字段,取长度是 O(1),C 字符串要遍历;② 二进制安全,SDS 可以存图片视频,C 字符串遇到
\0会截断;③ 自动扩容,避免缓冲区溢出。」
面试题:INCR 用来做什么?并发安全吗?
「做计数器,比如点赞、阅读量。是原子的——因为 Redis 命令执行是单线程的,多线程并发调用不会算错。这是它比用数据库
update ... set count = count + 1更高效的地方。」
2.2 Hash
命令
HSET k f v / HGET k f # 设/取单个字段
HMSET k f1 v1 f2 v2 # 批量设
HMGET k f1 f2 # 批量取
HGETALL k # 取全部(大key慎用)
HINCRBY k f 1 # 字段自增
HDEL k f # 删字段
典型场景
| 场景 | 用法 |
|---|---|
| 存对象 | HSET user:1 name tom age 20 |
| 购物车 | key=用户id,field=商品id,value=数量 |
| 字段级计数器 | 每个用户的各项指标 |
面试题:Hash 适合什么场景?
「适合存对象,且需要单独读写字段的场景,比如用户信息、购物车。相比 String 存 JSON,它的优势是能单独修改某个字段,不用整体读改写;而且内存占用通常更小。 集群下要注意:同一个 Hash 的所有字段必须在一个节点,所以大 Hash 会导致数据倾斜。」
2.3 List
命令
LPUSH k v / RPUSH k v # 左/右插入
LPOP k / RPOP k # 左/右弹出
BLPOP k 5 / BRPOP k 5 # 阻塞弹出(5秒超时)
LRANGE k 0 -1 # 取范围
LTRIM k 0 9 # 只保留前10个
LLEN k # 长度
典型场景
| 场景 | 用法 |
|---|---|
| 消息队列(简单) | LPUSH 生产 + BRPOP 消费(阻塞,不轮询) |
| 最新列表 | LPUSH + LTRIM 保持固定长度,如最新 100 条动态 |
| 栈/队列 | 同端进出是栈,异端进出是队列 |
面试题:用 List 做消息队列有什么问题?
标准答案(要说出不足,才显得懂)「List 做 MQ 有三个问题:① 没有 ack 机制——消息弹出就没了,消费者处理到一半挂了消息就丢了;② 不支持消费者组——不能多个消费者并行消费同一队列;③ 不支持消息回放。 所以 Redis 5.0 之后官方推荐用 Stream 做消息队列,它支持 ack 和消费者组。List 只适合简单的、丢一两条无所谓的场景。」
2.4 Set
命令
SADD k m / SREM k m # 增/删
SMEMBERS k # 取全部(大集合慎用)
SISMEMBER k m # 是否存在
SINTER k1 k2 # 交集
SUNION k1 k2 # 并集
SDIFF k1 k2 # 差集
SCARD k # 元素个数
SPOP k / SRANDMEMBER k # 随机弹出/随机取
典型场景
| 场景 | 用法 |
|---|---|
| 去重 | 统计独立访客 IP |
| 共同好友/共同关注 | SINTER 求交集(最经典) |
| 抽奖 | SPOP 抽出不重复的奖 |
| 标签/黑名单 | SISMEMBER 判断是否存在 |
面试题:怎么用 Redis 做「共同关注」?
「用 Set 的交集。把每个用户的关注列表存成一个 Set,求两个人的共同关注就是
SINTER user:1:follow user:2:follow。 注意数据量大或集群情况下,SINTER跨节点会有性能问题,可以用SINTERSTORE把结果存起来,或者业务上做分片优化。」
2.5 ZSet(重点)
命令
ZADD k 100 m # 添加,score=100
ZSCORE k m # 取分值
ZINCRBY k 1 m # 分值自增
ZRANGE k 0 -1 WITHSCORES # 升序取
ZREVRANGE k 0 9 # 降序取前10(排行榜)
ZRANK / ZREVRANK k m # 排名
ZRANGEBYSCORE k 90 100 # 按分数范围查
ZREM k m # 删除
典型场景
| 场景 | 用法 |
|---|---|
| 排行榜(最经典) | ZADD 更新分数,ZREVRANGE 0 9 取前十 |
| 延迟队列 | 时间戳当 score,定时任务取 ZRANGEBYSCORE 0 当前时间 |
| 带权重的队列 | 按优先级排序 |
| 范围查询 | 查分数区间内的元素 |
面试题:怎么做排行榜?
标准话术「用 ZSet。每个元素有一个 score,
ZADD更新分数,ZREVRANGE key 0 9取前十名。ZSet 是按 score 排序的,所以取排名和范围查询都很高效。 如果要展示「我的排名」,用ZREVRANK拿名次;如果要展示分数,ZREVRANGE ... WITHSCORES。」
面试题:怎么做延迟队列?
「用 ZSet,把任务的执行时间戳当 score。生产者
ZADD delay_queue 执行时间戳 任务id。消费者用一个定时任务,ZRANGEBYSCORE delay_queue 0 当前时间戳取出到期任务处理,处理完ZREM删除。 生产环境一般会用 Redisson 的RDelayedQueue,或者直接用专门的 MQ(如 RocketMQ 的延迟消息)。」
面试题:ZSet 为什么用跳表不用红黑树?(高频)
标准话术「三条理由:① 范围查询更自然——跳表底层是有序链表,做
ZRANGE这种范围查询找到起点顺序遍历就行;红黑树要中序遍历,实现更复杂。② 实现简单——跳表就是指针调整,代码比红黑树的旋转简单得多,不容易出 bug。③ 性能相当——两者查询都是 O(logN) 级别。 补充:Redis 的 ZSet 是跳表 + 哈希表双结构,跳表负责排序和范围查询,哈希表负责 O(1) 查某个元素的分数。」
2.6 类型选型速查(背这张表)
| 需求 | 选型 |
|---|---|
| 缓存对象(整体读写) | String + JSON |
| 存对象,要改单个字段 | Hash |
| 计数器、点赞、限流 | String(INCR) |
| 简单消息队列、最新列表 | List(正式 MQ 用 Stream) |
| 去重、共同好友、抽奖 | Set |
| 排行榜、延迟队列、范围查询 | ZSet |
| 签到打卡、活跃统计 | Bitmap |
| UV 去重统计 | HyperLogLog |
| 附近的人/店 | GEO |
| 可靠消息队列 | Stream |
| 实时广播通知 | Pub/Sub |
3. 其他类型
3.1 Bitmap(位图)
是什么:本质是 String,但按位操作,每个 bit 存 0/1。极省内存。
命令
SETBIT k offset 1 # 设置某位为1
GETBIT k offset # 获取某位
BITCOUNT k # 统计1的个数
BITOP AND dest k1 k2 # 按位运算
典型场景
| 场景 | 用法 | 省内存效果 |
|---|---|---|
| 签到打卡 | key=用户:月份,offset=日 | 1 个用户 1 个月只要 4 字节 |
| 活跃用户统计(DAU) | key=日期,offset=用户id,BITCOUNT 统计 | 100 万用户只要 125KB |
| 用户标签 | 性别、VIP 等布尔状态 | 极省 |
| 布隆过滤器 | 底层就是 Bitmap | — |
面试怎么答:「Bitmap 适合布尔状态的统计,最典型是签到。比如记录一个用户一个月签到情况,用 31 个 bit 就够,非常省内存。统计活跃用户数用
BITCOUNT。」
注意(会被追问):如果
offset很稀疏(比如用 10 亿的用户 id 当 offset),会分配大量空间。这时要考虑用哈希映射压缩,或者改用 HyperLogLog。
3.2 Bitfield(位域)★
是什么:也是 String 类型上的位操作,但和 Bitmap 有本质区别——
| | Bitmap | Bitfield |
|---|---|---|
| 操作单位 | 单个 bit | 一段连续 bit(一个整数) |
| 能存什么 | 只能是 0/1 | 整数(可指定几 bit、有无符号) |
| 典型用途 | 布尔状态(签到、是否) | 紧凑计数(分数、等级、状态码) |
一句话记住:Bitmap 存「是/否」,Bitfield 存「几」。
核心命令:BITFIELD
BITFIELD k GET u8 0 # 读:从第0位起,读8位无符号整数
BITFIELD k SET u8 0 255 # 写:从第0位起,写8位,值为255
BITFIELD k INCRBY u8 0 10 # 自增:第0位起8位,加10
BITFIELD k GET u8 0 GET u8 8 GET u8 16 # 一次读多个字段
关键:type 的写法 = u(无符号)或 i(有符号)+ 位数
| 类型 | 含义 | 取值范围 |
|---|---|---|
u8 | 无符号 8 位 | 0 ~ 255 |
u16 | 无符号 16 位 | 0 ~ 65535 |
i8 | 有符号 8 位 | -128 ~ 127 |
u1 | 无符号 1 位 | 0 ~ 1(等价于 Bitmap 的 SETBIT) |
u63 | 无符号 63 位 | 最大支持位数 |
范围限制:u 最多 63 位,i 最多 64 位。
# 语法简化(实用):u8 #0 表示「第 0 个 8 位字段」,会自动按类型宽度递增偏移。
BITFIELD k SET u8 #0 100 # 等价于 SET u8 0 100
BITFIELD k SET u8 #1 200 # 等价于 SET u8 8 200
溢出控制(面试加分点)
INCRBY 时如果超出范围,用 OVERFLOW 指定行为(默认 WRAP):
BITFIELD k OVERFLOW WRAP INCRBY u8 0 10 # 回绕(默认,255+1=0)
BITFIELD k OVERFLOW SAT INCRBY u8 0 10 # 饱和(255+1 仍是 255)
BITFIELD k OVERFLOW FAIL INCRBY u8 0 10 # 失败(返回 nil,不执行)
实际价值:用
SAT(饱和)可以做封顶计数器,比如「等级最高 255 级」,满了就不再增。
典型场景
| 场景 | 用法 |
|---|---|
| 紧凑存储多个小整数 | 一个游戏的玩家属性:等级(u8) + 段位(u8) + 连胜(u8),3 字节搞定 |
| 封顶计数器 | OVERFLOW SAT,如签到次数上限、等级上限 |
| 时间序列的紧凑存储 | 每小时一个计数器,按位偏移排列 |
| 节省内存的标记位集合 | 多个 2-4 位的枚举状态(如订单状态) |
和 Bitmap 的取舍(面试常考)
| 维度 | Bitmap | Bitfield |
|---|---|---|
| 内存 | 1 bit / 状态 | 按字段宽度算,存整数略费 |
| 能存的值 | 只能 0/1 | 任意整数 |
| 适合 | 大量布尔状态 | 多个小整数紧密排列 |
面试话术(背)「Bitfield 是 String 类型上的位域操作,和 Bitmap 的区别是:Bitmap 操作单个 bit,只能存 0/1;Bitfield 操作一段连续 bit,可以存指定宽度的整数。用法是
BITFIELD key SET u8 0 255,u8表示无符号 8 位。它支持一次操作多个字段,也支持INCRBY自增。 最实用的两点:① 紧凑存储——比如把多个小整数(等级、段位、连胜)打包到一个 key 里,几个字节就够了;② 溢出控制——用OVERFLOW SAT做封顶计数器,比如等级上限。 简单记:要存「是/否」用 Bitmap,要存「几」用 Bitfield。」
追问:Bitfield 比用多个 String 好在哪?「省内存和网络往返。比如存 10 个 0-100 的数值,用 10 个 String 是 10 个 key、10 次 IO;用 Bitfield 一个 key 就够了,而且
BITFIELD一条命令能同时读写多个字段。」
3.3 HyperLogLog(基数统计)
是什么:概率性数据结构,用来估算不重复元素个数(基数)。固定只占 12KB,标准误差 0.81%。
命令
PFADD k e1 e2 # 添加元素
PFCOUNT k # 统计基数
PFMERGE dest k1 k2 # 合并多个
典型场景
| 场景 | 说明 |
|---|---|
| UV 统计 | 统计网站独立访客数 |
| 搜索词去重统计 | 有多少个不同的搜索词 |
| 海量去重 | 数据量大、不需要精确值的场景 |
和 Bitmap 怎么选
| | Bitmap | HyperLogLog |
|---|---|---|
| 精度 | 精确 | 0.81% 误差 |
| 内存 | 取决于最大 offset | 固定 12KB |
| 适合 | offset 密集(如用户id连续) | 用户 id 稀疏、海量数据 |
面试话术「HyperLogLog 是用来做基数统计的,就是统计有多少个不重复元素,比如 UV。它的特点是固定占 12KB,误差 0.81%,非常适合海量数据的去重统计。 和 Bitmap 的区别:Bitmap 是精确的,但内存取决于最大 offset;HyperLogLog 内存固定,但有误差。如果用户 id 很稀疏(比如是雪花 id),用 Bitmap 会浪费巨大空间,这时候就该用 HyperLogLog。局限是它不能返回具体是哪些元素,也不能删单个元素。」
3.4 GEO(地理位置)
是什么:基于 ZSet 实现的地理位置功能,用 GeoHash 把经纬度编码成分数。
命令
GEOADD k 116.40 39.90 北京 # 添加位置
GEOPOS k 北京 # 查坐标
GEODIST k 北京 上海 km # 两点距离
GEORADIUS k 116.40 39.90 10 km # 查半径内的点
GEOSEARCH k FROMMEMBER 北京 BYRADIUS 10 km # 更灵活的查询
典型场景
- 附近的人 / 附近的店
- 打车软件的司机周边搜索
- 外卖配送范围计算
面试怎么答:「GEO 底层是 ZSet,用 GeoHash 把经纬度编码成一个数值当 score。用
GEOADD存位置,GEORADIUS或GEOSEARCH查附近的点,GEODIST算距离。适合附近的人、附近的店这类场景。」
3.5 Stream(可靠消息队列)★
这是「Redis 怎么做消息队列」的标准答案,必须会。
命令
XADD k * f v # 添加消息(*自动生成ID)
XLEN k # 消息数
XRANGE k - + # 范围查询
XREAD [BLOCK 1000] STREAMS k $ # 读取最新
XGROUP CREATE k group $ # 创建消费者组
XREADGROUP GROUP g c STREAMS k > # 组内读取新消息
XACK k g id # 确认消息
XPENDING k g # 查看未确认的
XCLAIM k g c 60000 id # 转移消息给别的消费者
XTRIM k MAXLEN 1000 # 裁剪长度
核心概念(讲这三个就够)
| 概念 | 作用 |
|---|---|
| 消费者组 | 组内竞争消费(一条消息只给组内一个消费者),组间广播(不同组各自消费全部消息)。类似 Kafka |
| XACK 确认 | 消息读完要手动确认,不确认就留在待处理列表里 |
| PEL 待处理列表 | 记录已投递但未确认的消息。消费者挂了,可以用 XCLAIM 转移给别人——这就是不丢消息的保证 |
和 List / Pub-Sub 的对比(必考)
| | List | Pub/Sub | Stream |
|---|---|---|---|
| 消息持久化 | ✅ | ❌ | ✅ |
| ack 确认 | ❌ | ❌ | ✅ |
| 消费者组 | ❌ | ❌ | ✅ |
| 消息回放 | ❌ | ❌ | ✅ |
| 适用 | 简单队列 | 实时广播 | 可靠消息队列 |
典型场景
- 订单超时未支付取消(延迟 + 可靠消费)
- 异步任务队列(要保证任务不丢)
- 消息广播:多个消费者组各自处理(如「下单」同时触发积分、通知、库存)
面试话术(重点背)「Redis 做消息队列要用 Stream,不要用 List。Stream 相比 List 有三个核心优势:① 有 ack 机制——消息读完要
XACK确认,没确认的留在待处理列表 PEL 里;② 支持消费者组——组内竞争、组间广播,类似 Kafka;③ 消息可持久保留和回放。 关键设计是 PEL 待处理列表——消费者读了但没确认的消息都在里面,消费者挂了可以用XCLAIM转移给别的消费者,保证消息不丢。 不过 Stream 也不能替代 Kafka,因为消息存内存、容量有限,没有分区和副本,只适合轻量级、短周期的任务队列。」
3.6 Pub/Sub(发布订阅)
命令
SUBSCRIBE ch1 ch2 # 订阅频道
PUBLISH ch msg # 发布消息
PSUBSCRIBE news.* # 模式订阅(支持通配符)
PUBSUB CHANNELS # 查看活跃频道
最关键的特性:消息不持久化、不保证送达。
- 发布时只有在线的订阅者能收到。
- 订阅者掉线期间的消息直接丢失。
- 没有 ack 机制。
典型场景
- 实时通知、聊天室广播(丢一两条无所谓)
- 配置刷新通知(配置中心推送)
- 哨兵之间通信
不适合:需要可靠投递、需要历史消息的场景(那是 Stream 的活)。
面试话术「Pub/Sub 是发布订阅模式,但它不持久化、不保证送达——订阅者掉线期间的消息直接丢失,也没有 ack。所以它只适合实时广播这类丢一两条无所谓的场景,比如聊天室、配置推送。 如果需要可靠投递,要用 Stream。这两个是面试常考的对比。」
加分点(集群):「在 Cluster 模式下 Pub/Sub 是全局广播的,消息量大会消耗集群带宽。6.0 之后加了 Sharded Pub/Sub 来解决。」
3.7 其他类型速查
| 类型 | 一句话 | 典型场景 |
|---|---|---|
| Bitmap | 单 bit 操作,只能存 0/1 | 签到、DAU |
| Bitfield | 一段连续 bit 存整数,可指定宽度 | 紧凑存储多个小整数、封顶计数 |
| HyperLogLog | 12KB 估算基数,0.81% 误差 | UV 统计 |
| GEO | 地理位置 | 附近的人 |
| Stream | 可靠消息队列,有 ack | 订单超时、异步任务 |
| Pub/Sub | 实时广播,不保证送达 | 聊天室、配置推送 |
4. 缓存三大问题与一致性(必考)
4.1 总览表(先背这张)
| 问题 | 一句话 | 解法 |
|---|---|---|
| 穿透 | 查不存在的数据,每次穿到 DB | 布隆过滤器、缓存空值 |
| 击穿 | 单个热点 key 过期瞬间,大量请求打 DB | 互斥锁、逻辑过期 |
| 雪崩 | 大量 key 同时过期或 Redis 挂 | 随机过期、多级缓存、熔断降级 |
一句话区分:
穿透是「查没有的」,击穿是「一个热点过期」,雪崩是「一片同时失效」。
4.2 缓存穿透
场景:请求查一个数据库里也不存在的数据(如 id=-1),缓存里没有、DB 里也没有,所以不会回写缓存,导致每次请求都打到 DB。恶意攻击常用这种方式。
方案一:缓存空值
- 查不到也缓存起来:
SET id:-1 "" EX 60。 - 优点:简单。
- 缺点:占内存(攻击者用随机 id 可撑爆);数据窗口期不一致(这期间 DB 新增了该数据,缓存还是空值)。
方案二:布隆过滤器(推荐)
- 把 DB 中存在的 key 全部预加载进布隆过滤器。
- 请求进来先过过滤器:说不存在就直接拒绝,说存在才继续查缓存/DB。
- 特点:不会漏判,但可能误判(把不存在的判成存在);不支持删除元素。
面试话术(背)「缓存穿透是查 DB 里也不存在的数据,缓存拦不住。两个方案: ① 缓存空值——查不到也缓存一个空值,简单但占内存、有窗口期不一致; ② 布隆过滤器——把存在的 key 预加载进去,不存在的直接拦掉,不会漏判但可能误判,而且不支持删除。 实际一般两者结合:布隆过滤器挡大部分,空值兜底。」
追问:布隆过滤器为什么误判?「因为多个元素哈希后可能占用相同的位,导致判断别的元素时那些位是 1,就误认为存在。位数组越长、哈希函数越合适,误判率越低。 布隆过滤器底层是Bitmap+hash,为了避免ID过大所以用hash以节省内存」
4.3 缓存击穿
场景:某个热点 key 刚好过期,瞬间大量并发请求都没命中缓存,全部去查 DB 重建缓存,DB 压力剧增。
和穿透的区别:穿透是数据根本不存在;击穿是数据存在,只是热点过期那一刻穿透。
方案一:互斥锁
- 缓存未命中时,只让一个线程去查 DB 重建,其他线程短暂等待后重试。
- 优点:保证只有一个请求打 DB。
- 缺点:牺牲可用性(其他线程要等);有死锁风险,要设好锁超时。
方案二:逻辑过期(热点数据永不过期)★
- 物理上不设 TTL,在 value 里存一个逻辑过期时间。
- 读到数据时判断:没过期直接返回;过期了则异步启动线程重建,当前请求先返回旧数据。
- 优点:请求永远不落空、无等待。
- 缺点:返回的是旧数据,需要业务能容忍;实现更复杂。
面试话术(背)「击穿是单个热点 key 过期瞬间,大量请求同时打 DB。两个方案: ① 互斥锁——只让一个线程重建缓存,其他等待,缺点是牺牲可用性、有死锁风险; ② 逻辑过期——物理上不设过期时间,value 里存一个逻辑过期时间,过期后异步重建、当前请求先返回旧值。优点是请求不落空,缺点是可能读到旧数据。 热点数据一般用逻辑过期,因为互斥锁会让用户感知到延迟。」
4.4 缓存雪崩
场景:大量 key 在同一时间过期(比如凌晨批量刷新),或者 Redis 整体宕机,导致所有请求瞬间涌向 DB,DB 崩溃。
和击穿的区别:击穿是单个热点 key;雪崩是大量 key 或整个缓存不可用。
四个方案(从预防到兜底)
| 层次 | 方案 | 说明 |
|---|---|---|
| 事前 | 随机过期时间 | 基础时间 + random(0, N),把过期点打散 |
| 事前 | 集群高可用 | 用哨兵/Cluster,减少 Redis 整体宕机概率 |
| 事中 | 多级缓存 | 本地缓存(Caffeine)+ Redis 双层,Redis 挂了本地还能扛 |
| 事后 | 熔断降级 | DB 压力过大时主动拒绝部分请求,返回降级数据 |
熔断降级是重点:「最后一道防线是熔断降级——当 DB 压力太大时,主动返回降级数据(默认值、错误提示页),保 DB 不死。因为 DB 挂了整个系统就完了,宁可拒绝一部分请求。」
面试话术(背)「雪崩是大量 key 同时过期,或者 Redis 整体挂掉。分三层防: ① 事前——过期时间加随机值打散,用集群高可用减少整体宕机; ② 事中——加本地缓存做多级缓存,Redis 挂了还能扛一部分; ③ 事后——熔断降级,DB 撑不住时返回降级数据保住 DB。 熔断降级是最关键的兜底,因为 DB 一旦挂了整个系统就崩了。」
4.5 缓存一致性(必考,重点)
问题:数据在缓存和 DB 各有一份,更新时怎么保证一致?
结论先给:Cache Aside(先更新 DB,再删缓存)
四种策略对比
| 策略 | 问题 | 结论 |
|---|---|---|
| 先更新缓存,再更新 DB | DB 更新失败 → 缓存是脏数据 | ❌ |
| 先更新 DB,再更新缓存 | 并发写会乱序、无效计算 | ❌ |
| 先删缓存,再更新 DB | 并发读时会把旧值写回缓存 | ⚠️ |
| 先更新 DB,再删缓存 | 极小并发窗口 | ✅ 推荐 |
为什么是「删」而不是「更新」:
- 懒加载——删了下次读时再重建,不用做无效计算。
- 避免写放大——如果缓存值是复杂聚合结果,每次更新都算一遍太浪费。
- 避免并发乱序——两个更新请求同时来,删缓存不会搞乱顺序。
「先更新 DB 再删缓存」的已知缺陷(面试常考):
- 请求 A 读缓存未命中 → 读 DB 拿到旧值 v1
- 请求 B 更新 DB 为 v2 → 删除缓存
- 请求 A 把 v1 写回缓存 → 缓存是旧值,不一致
但概率很低——要求「A 读 DB 到写缓存」这段时间里恰好插入了一次写。读通常比写快得多,窗口极窄。
增强方案
方案一:延迟双删
- 流程:先删缓存 → 更新 DB → 延迟(如 500ms)再删一次。
- 目的:把那段窗口里可能被写回的旧值删掉。
- 缺点:延迟时间不好精确估计(要大于读请求的最长耗时)。
方案二:订阅 binlog(Canal)★
- Canal 伪装成 MySQL 从库订阅 binlog,数据变更后异步删缓存。
- 优点:业务代码零侵入;不容易漏删(改 DB 一定有 binlog);解耦(可以同时更新 ES、发消息)。
- 缺点:引入中间件,架构复杂;有异步延迟。
方案三:消息队列补偿
- 删除失败时把操作丢进 MQ 重试,保证最终删掉。
方案四:加锁(强一致)
- 读写加锁保证互斥,性能代价极大,一般不推荐。
面试话术(背,这套话术能拿高分)「一致性问题的核心是:缓存和 DB 都存了同一份数据,更新时必然有窗口期不一致。
主流方案是 Cache Aside——先更新 DB,再删除缓存。用删除而不是更新,是因为删除是懒加载,避免无效计算和并发乱序。
它理论上有个小问题:读请求读到旧值又写回缓存。但这个窗口概率很低,因为读比写快得多。要增强可以加延迟双删——更新 DB 后延迟再删一次,把那个窗口的旧值清掉。
更可靠的做法是 Canal 订阅 MySQL 的 binlog 异步删缓存,业务无侵入、不容易漏删,代价是架构更复杂。
最后强调一点:缓存场景很难做到强一致,一般业务追求最终一致就够了。」
追问:能保证强一致吗?「很难。读写并发下总有窗口期,要强一致得用分布式锁或 2PC,性能代价太大,不适合缓存场景。所以缓存一般是最终一致,关键业务直接查 DB 不走缓存。」
4.6 缓存其他常见问题
| 问题 | 说明 | 解决 |
|---|---|---|
| 缓存预热 | 刚上线时缓存是空的 | 启动时提前加载热点数据 |
| 缓存降级 | Redis 故障时 | 降级为查 DB,或返回默认值(注意别把 DB 打挂) |
| 大 key | 见 1.3 | 拆分、压缩、UNLINK 删 |
| 热 key | 见 1.3 | 本地缓存、分散 key |
5. 分布式锁(必考)
5.1 为什么需要
单机锁的问题:std::mutex 只在一个进程内有效。多台服务器操作同一资源(扣库存、抢单)时,进程内的锁管不住别的机器。
一句话:分布式锁解决跨进程、跨机器的互斥。
一个好的分布式锁要满足:
| 要求 | 不满足的后果 |
|---|---|
| 互斥 | 数据错乱 |
| 防死锁 | 系统卡死 |
| 防误删 | 删了别人的锁,互斥被破坏 |
| 可重入(可选) | 自调用死锁 |
| 高可用 | 锁服务挂了,服务不可用 |
5.2 正确实现(直接背这套)
加锁
SET lock_key unique_value NX PX 30000
NX:key 不存在才设置成功(= 抢到锁)。PX 30000:30 秒后自动过期(防死锁)。unique_value:唯一标识(UUID + 线程ID),用于防止误删。
为什么必须一条命令:如果用 SETNX + EXPIRE 两条,中间宕机就会导致锁永不过期 → 死锁。
释放锁(Lua 脚本)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 先比对 value 再删除:防止 A 超时后删了 B 的锁。
- 必须用 Lua:保证「判断 + 删除」是原子的,否则判断后锁被抢走就会删错。
为什么要唯一 value:
客户端 A 拿到锁,业务执行超时(超过 TTL),锁自动过期;客户端 B 拿到锁;A 处理完执行
DEL,把 B 的锁删了。所以要用唯一 value + Lua 比对。
5.3 锁续期:看门狗
问题:即使有唯一 value,如果业务执行时间 > 锁 TTL,锁还是提前释放了,互斥被破坏。
两种思路:
- TTL 设长一点:简单,但不可靠(无法预估业务耗时),设太长又影响故障恢复。
- 自动续期(看门狗)★:加锁成功后启动一个后台任务,每隔一段时间(默认 TTL/3,即每 10 秒)检查业务是否还持有锁,是就把 TTL 重置回 30 秒。业务结束就停止续期。
注意:Redisson 的看门狗只在你不指定 TTL 时才启用。如果你自己 tryLock(10, SECONDS) 指定了 TTL,看门狗不生效。
为什么进程挂了没问题:看门狗线程随进程一起停,锁就按最后的 TTL 自然过期——这正是期望的行为(宕机就该释放锁)。
5.4 可重入
什么是可重入:同一线程多次获取同一把锁都该成功(比如 A 方法加锁后调 B 方法,B 也加同一把锁,不该死锁)。
实现:用 Hash,field 是客户端唯一标识,value 是重入次数。
- 加锁:field 已存在 → 次数 +1;不存在 → 设为 1。
- 解锁:次数 -1,减到 0 才真正删锁。
面试怎么答:「可重入用 Hash 实现——field 存客户端标识,value 存重入次数。加锁时同 field 就 +1,解锁时 -1,减到 0 才真正删除。实际生产直接用 Redisson 的 RLock,这些都封装好了。」
5.5 Redlock 与争议(了解即可)
为什么需要:单机 Redis 在主从切换时会丢锁——A 在 master 拿到锁,master 还没同步就宕机,slave 升为 master(没这把锁),B 也能拿到锁,A、B 同时持锁。
Redlock 方案:向 5 个独立的 Redis 实例加锁,超过半数(3 个)成功才算拿到,且总耗时小于锁有效期。
争议(简单了解,能说出双方观点就行):
| 观点 | 内容 |
|---|---|
| Kleppmann 质疑 | 依赖时钟同步,时钟漂移就不安全;GC 停顿会导致锁过期但客户端不知道;建议用 ZooKeeper |
| antirez 回应 | 现实中时钟漂移可接受,这是工程折中 |
面试话术(背,含”我的看法”是加分)「Redlock 是为解决单机 Redis 主从切换丢锁的问题,向 5 个独立实例加锁、过半数成功才算拿到。 但它有争议:Kleppmann 认为它依赖时钟同步,时钟漂移或者 GC 停顿的情况下不安全,建议用 ZooKeeper;antirez 认为这是工程折中。我的看法是:大多数业务用单实例锁 + 看门狗就够了;真正需要强一致的场景应该用 ZooKeeper 或 etcd。」
5.6 分布式锁方案对比
| 实现 | 一致性 | 性能 | 适用场景 |
|---|---|---|---|
| Redis | 最终一致(AP) | 高 | 大多数业务(防重复提交、避免重复计算) |
| ZooKeeper | 强一致(CP) | 中 | 强一致场景(如金融扣款) |
| etcd | 强一致(CP) | 中 | 云原生场景 |
| 数据库 | 依赖 DB | 低 | 简单、低频场景 |
加分回答:「选型看业务对锁失效的容忍度。大多数业务(比如防重复提交)容忍极小概率失效,用 Redis 性能好又简单;只有金融级的场景才需要 ZooKeeper 的强一致。」
5.7 生产建议
实际工作中别自己写,用 Redisson。 它封装了:
- 加锁/解锁的 Lua 脚本
- 看门狗自动续期
- 可重入(Hash 计数)
- 公平锁、读写锁、Redlock 等高级功能
6. 事务 / 管道 / Lua
6.1 事务(MULTI/EXEC)
命令
MULTI # 开启事务
SET k1 v1 # 入队(返回 QUEUED,不执行)
SET k2 v2 # 入队
EXEC # 执行所有命令
DISCARD # 放弃事务
WATCH k # 乐观锁:监视 key,被改则 EXEC 失败
三个关键特性(面试必问)
① 不支持回滚(最反直觉)
- 入队时错误(如语法错):整个事务都不执行。
- 执行时错误(如对 String 用
LPUSH):其他命令照常执行,只有出错的失败。 - 为什么这样设计:Redis 认为执行期错误是编程错误,应该开发阶段发现;回滚需要 undo log,成本高,违背 Redis 简单高效的定位。
② 严格说不是原子性的
- 只保证「命令按顺序执行、不被打断」,不保证全成功或全失败。
③ 隔离性由单线程保证
- 执行期间不会插入其他命令,天然串行化。
WATCH 做乐观锁(秒杀场景)
WATCH stock
val = GET stock
if val > 0:
MULTI
DECR stock
EXEC # 如果 stock 被别人改了,EXEC 返回 nil,重试
面试话术「Redis 事务用 MULTI 开、EXEC 执行,命令先入队再批量执行。要讲清三点: ① 不支持回滚——入队时错误整个不执行,执行时错误其他命令照常执行。设计原因是为了保持简单和性能,Redis 认为执行期错误是编程错误,不该靠回滚兜底。 ② 严格说不是原子性的——只保证不被打断,不保证全成功或全失败。 ③ 隔离性由单线程保证。 另外 WATCH 提供乐观锁能力,可以做秒杀扣库存这类 CAS 操作。」
追问:那怎么做真正的原子操作? → 用 Lua 脚本,它在 Redis 里整体原子执行,还能写条件判断。
6.2 Pipeline(管道)
是什么:客户端把多条命令一次性发过去、一次性收响应,减少网络往返(RTT)。
直观对比:
- 不用 Pipeline:
命令1 → 等响应 → 命令2 → 等响应 → ...(N 个 RTT) - 用 Pipeline:
命令1+2+...+N → 一次发送 → 一次收响应(1 个 RTT)
关键点:
- 不是原子操作——服务端还是逐条执行,中间可能插入别的命令。
- 主要目的是省网络 RTT,不是保证原子性。
使用注意:
- 要分批——一次发太多(如几十万条),服务端要缓存所有响应,内存暴增。建议每批 1000 条左右。
- 适合批量写入、批量查询(初始化数据、批量删除)。
6.3 Lua 脚本
命令
EVAL "script" 1 key arg # 执行脚本
SCRIPT LOAD "script" # 预加载,返回 SHA1
EVALSHA sha1 1 key arg # 按 SHA1 执行(省带宽)
核心优势:
- 原子执行——执行期间不处理其他命令。这是它比事务强的地方(事务不回滚,Lua 能做条件判断 + 原子执行)。
- 减少网络往返——业务判断打包到服务端。
- 可复用——预加载后用 SHA1 调用。
典型应用:
- 分布式锁的加解锁(前面讲的 Lua)
- 限流(滑动窗口、令牌桶)
- 秒杀扣库存(判断库存 + 扣减,原子完成)
- CAS 操作
注意:脚本要快(单线程下会阻塞其他命令)。
6.4 三者对比(必背)
| 维度 | Pipeline | 事务 MULTI/EXEC | Lua 脚本 |
|---|---|---|---|
| 是否原子 | ❌ | ⚠️ 顺序执行不被打断,但不回滚 | ✅ 整体原子 |
| 能否写逻辑 | ❌ | ❌ 只能排队 | ✅ 支持条件判断 |
| 主要目的 | 省 RTT | 打包执行 | 原子 + 逻辑 |
| 典型场景 | 批量读写 | 弱事务场景 | 加锁、限流、秒杀 |
面试话术「这三个经常被搞混。Pipeline 是客户端批量发命令,主要为了省网络 RTT,不是原子操作;事务是 MULTI/EXEC 打包执行,但不支持回滚;Lua 脚本是在服务端原子执行,还能写条件判断。所以真正需要原子性 + 逻辑判断的场景要用 Lua,比如分布式锁、秒杀扣库存。」
7. 持久化与高可用:怎么选
7.1 持久化:选哪个
两种方式对比
| | RDB | AOF |
|---|---|---|
| 原理 | 定时快照(二进制) | 记录每条写命令 |
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 差(可能丢几分钟) | 好(最多丢 1 秒) |
| 适合 | 冷备份 | 高可靠要求 |
AOF 刷盘策略(决定丢多少数据)
| 策略 | 行为 | 丢数据 | 性能 |
|---|---|---|---|
always | 每条都写盘 | 几乎不丢 | 最差 |
everysec(默认) | 每秒写盘 | 最多丢 1 秒 | 折中 |
no | 交给操作系统 | 丢得多 | 最好 |
结论:用混合持久化(4.0+,默认推荐)
- AOF 重写时,前半是 RDB 快照,后半是 AOF 命令。
- 恢复时先加载快照(快),再重放增量(全)——兼顾速度和安全性。
- 配置:
appendonly yes+aof-use-rdb-preamble yes
面试话术「RDB 是快照,文件小、恢复快,但可能丢几分钟数据;AOF 是记日志,最多丢 1 秒,但文件大、恢复慢。所以 4.0 之后推荐混合持久化——AOF 重写时前半用 RDB、后半用 AOF,恢复时先加载快照再重放增量,兼顾速度和安全性。 AOF 的刷盘策略一般用 everysec,最多丢 1 秒,是性能和安全的平衡点。」
面试题:Redis 数据会丢吗?
「会。RDB 模式下,如果 Redis 挂了,会丢掉最后一次快照之后的所有数据,可能几分钟。AOF everysec 模式下最多丢 1 秒数据。主从复制是异步的,master 挂了可能丢掉未同步的写。 所以 Redis 不适合当唯一的数据存储,只能做缓存或可重建的数据。」
7.2 高可用:三层方案
| 方案 | 解决什么 | 分片 | 故障转移 | 写能力 |
|---|---|---|---|---|
| 主从 | 读扩展、备份 | ❌ | ❌ 手动 | 单机 |
| 哨兵 | 自动故障转移 | ❌ | ✅ 自动 | 单机 |
| Cluster | 扩展 + 高可用 | ✅ | ✅ 自动 | 多机水平扩展 |
怎么选:
- 只要读扩展和备份 → 主从
- 要高可用但数据量不大 → 哨兵
- 数据量大 / 写压力大 → Cluster
主从复制
- 一台 master 写,多台 slave 读,master 把写命令异步同步给 slave。
- 全量同步:slave 首次连接,master 生成 RDB 发过去,期间的写命令补发。
- 增量同步:短暂断线重连时,master 只发缺失的那部分命令(基于积压缓冲区 + offset)。
- 问题:master 挂了不会自动切换,要手动。→ 需要哨兵。
面试话术:「主从复制是异步的,master 把写命令发给 slave,slave 执行。全量同步是 master 生成 RDB 发过去再补发增量命令;短暂断线走增量同步。主从的问题是不会自动故障转移,master 挂了要手动切,所以要配哨兵。」
哨兵(Sentinel)
- 独立的进程,监控 Redis 节点,master 挂了自动选一个 slave 当新 master。
- 故障转移流程:
- 单个哨兵发现 master 没响应 → 主观下线。
- 向其他哨兵确认,多数认为挂了 → 客观下线。
- 哨兵之间选出 leader,从 slave 里挑新的 master(按优先级 → 数据最新 → 运行时间长)。
- 通知客户端新 master 地址。
- 为什么要多哨兵确认:防止单个哨兵因网络抖动误判。
面试话术「哨兵解决主从不自动切换的问题。流程是:单个哨兵发现 master 挂了先主观下线,再向其他哨兵确认,多数认为挂了才客观下线,然后选 leader 从 slave 里挑新 master——优先看优先级,再看谁的数据最新。主观到客观这个两步是为了防止单哨兵误判。」
Redis Cluster
- 多 master 分片,每个 master 存一部分数据,解决单机内存和写性能上限。
- 16384 个哈希槽:每个 key 用
CRC16(key) % 16384算属于哪个槽,每个 master 负责一部分槽。 - 重定向:
- MOVED:槽永久属于别的节点 → 客户端更新本地路由缓存。
- ASK:槽正在迁移中 → 临时重定向,不更新缓存。
- 高可用:每个 master 可挂 slave,master 挂了自动选 slave 顶上。
- 限制:
- 跨槽的多 key 操作会失败(如
MGET k1 k2若在不同槽)→ 用 hash tag 解决({tag}k1、{tag}k2进同一个槽)。 - 只有 db0,不支持多数据库。
- 不保证强一致(异步复制)。
- 跨槽的多 key 操作会失败(如
面试话术「Cluster 用 16384 个哈希槽分片,key 按 CRC16 取模定位到槽,每个 master 负责一部分槽。请求的 key 不在当前节点会返回重定向:MOVED 是永久归属,客户端要更新路由缓存;ASK 是槽正在迁移,临时重定向,发起前要先发 ASKING 命令。 Cluster 自带故障转移,但不保证强一致,而且跨槽的多 key 操作会失败,要用 hash tag 让它们落同一个槽。」
8. 生产规范与常见坑
8.1 Key 命名规范
业务名:对象名:id:字段
例:user:profile:1001:name
order:detail:20230901
- 用冒号分层,便于管理和排查。
- 避免过长——key 本身占内存。
- 避免无意义——
a1、b2这种后期没法维护。
8.2 必须遵守的生产规范
| 规范 | 原因 | 正确做法 |
|---|---|---|
禁用 KEYS * | 阻塞主线程 | 用 SCAN |
禁用 FLUSHALL | 阻塞 + 数据全丢 | 用 FLUSHALL ASYNC,或按 key 删 |
删大 key 用 UNLINK | DEL 会阻塞 | UNLINK 异步删 |
| 批量取要分批 | 大集合返回慢 | HSCAN/SSCAN,或分页 |
| 必须设过期时间 | 不设会内存泄漏 | 缓存数据都要设 TTL |
| 过期时间加随机值 | 防止同时失效(雪崩) | 基础时间 + random(0,300) |
| 避免大 key | 阻塞 + 数据倾斜 | 拆分、压缩 |
| 注意热 key | 单节点压力大 | 本地缓存、分散 key |
| 用 Pipeline 时分批 | 响应缓存占内存 | 每批 1000 条左右 |
8.3 内存淘汰策略(8 种)
当内存达到 maxmemory 时的淘汰策略:
| 策略 | 说明 | 推荐场景 |
|---|---|---|
noeviction | 不淘汰,写入直接报错(默认) | 不能丢数据 |
allkeys-lru | 所有 key 中按 LRU 淘汰 | 纯缓存场景(推荐) |
allkeys-lfu | 所有 key 中按 LFU 淘汰 | 有明确冷热数据 |
allkeys-random | 所有 key 中随机淘汰 | 访问均匀 |
volatile-lru | 只在设了过期时间的 key 中 LRU | 缓存 + 持久化混合 |
volatile-lfu | 同上但用 LFU | 同上 |
volatile-random | 同上但随机 | — |
volatile-ttl | 优先淘汰 TTL 短的 | 希望快过期的先走 |
LRU vs LFU:LRU 淘汰「最久没用」的,LFU 淘汰「使用频率最低」的。缓存场景一般用 LRU;如果担心偶发的一次性访问污染缓存,用 LFU。
面试怎么答:「做缓存一般用 allkeys-lru,淘汰最久没用的。如果既做缓存又存持久数据,用 volatile-lru,只淘汰设了过期时间的。如果希望快过期的优先淘汰,用 volatile-ttl。」
8.4 过期删除策略
Redis 用两种策略结合(面试会问):
- 惰性删除:访问 key 时才检查是否过期,过期就删。
- 优点:不浪费 CPU。
- 缺点:不访问的 key 永远不删,占内存。
- 定期删除:每 100ms 随机抽查一部分 key,删掉过期的。
- 优点:能清理不访问的 key。
- 缺点:抽查是随机的,可能有漏网。
为什么两者结合:只用惰性删除会导致内存泄漏(冷数据永不删);只用定期删除会浪费 CPU。结合起来既不太费 CPU,又能清理大部分过期 key。如果还有漏网,就靠内存淘汰策略兜底。
8.5 常见坑清单
| 坑 | 后果 | 避免方法 |
|---|---|---|
| 缓存和 DB 不一致 | 用户看到旧数据 | Cache Aside + 延迟双删/Canal |
| 缓存雪崩 | DB 被打爆 | 随机过期 + 熔断降级 |
| 大 key | 阻塞、倾斜 | 拆分、UNLINK |
| 热 key | 单节点打爆 | 本地缓存、分散 key |
| 没设 TTL | 内存泄漏 | 缓存必须设过期 |
| 分布式锁误删 | 互斥失效 | 唯一 value + Lua |
| 事务不回滚被误解 | 数据不一致 | 用 Lua 代替事务 |
| 集群跨槽操作失败 | 报错 | 用 hash tag |
| 主从异步丢数据 | 数据丢失 | 用 WAIT 或加持久化 |
9. 面试题速查(考前背这个)
9.1 高频题 + 一句话答案
| 面试题 | 一句话答案 |
|---|---|
| Redis 是什么 | 内存 KV 数据库,主用缓存 |
| 为什么快 | 内存 + 单线程免锁 + epoll + 定制结构 |
| 和 Memcached 区别 | 数据结构丰富 + 支持持久化 + 有集群 |
| 为什么单线程 | 瓶颈在 IO 不在 CPU,多线程锁复杂 |
| 5 种类型 | String/Hash/List/Set/ZSet |
| 缓存对象用 String 还是 Hash | 整体读写 String,改单字段 Hash |
| 排行榜怎么做 | ZSet,score 排序 + ZREVRANGE |
| 延迟队列怎么做 | ZSet,时间戳当 score |
| 共同关注怎么做 | Set 的 SINTER 交集 |
| 签到怎么记录 | Bitmap,SETBIT + BITCOUNT |
| Bitmap 和 Bitfield 区别 | Bitmap 存 0/1,Bitfield 存指定宽度整数 |
| Bitfield 用在哪 | 紧凑存多个小整数、OVERFLOW SAT 做封顶计数 |
| UV 怎么统计 | HyperLogLog,固定 12KB |
| 消息队列怎么做 | Stream(有 ack + 消费者组),别用 List |
| 为什么不用 List 做 MQ | 没 ack、没消费者组、不能回放 |
| Pub/Sub 和 Stream 区别 | Pub/Sub 不持久化不保证送达 |
| 缓存穿透 | 查不存在的数据,布隆过滤器 + 空值缓存 |
| 缓存击穿 | 热点 key 过期,互斥锁 + 逻辑过期 |
| 缓存雪崩 | 大量 key 同时过期,随机过期 + 熔断降级 |
| 穿透/击穿/雪崩区别 | 不存在 / 一个过期 / 一片失效 |
| 缓存一致性怎么做 | Cache Aside,先更新 DB 再删缓存 |
| 为什么删缓存不更新缓存 | 懒加载,避免无效计算和并发乱序 |
| 延迟双删 | 更新 DB 后延迟再删一次 |
| 分布式锁怎么做 | SET NX PX + 唯一值 + Lua + 看门狗 |
| 为什么要唯一 value | 防误删别人的锁 |
| 为什么用 Lua 释放 | 保证判断+删除原子 |
| 看门狗是什么 | 后台任务定期续期 TTL |
| Redlock 争议 | Kleppmann 说依赖时钟不安全,antirez 说是折中 |
| 事务支持回滚吗 | 不支持,为简单和性能 |
| 事务原子吗 | 严格说不是,只保证不被打断 |
| Pipeline 和事务区别 | Pipeline 省 RTT 不原子;事务打包但不回滚 |
| 什么时候用 Lua | 需要原子 + 逻辑判断,如锁、限流、秒杀 |
| 持久化怎么选 | 混合持久化(RDB+AOF) |
| RDB 和 AOF 区别 | RDB 快但丢数据,AOF 慢但可靠 |
| everysec 丢多少 | 最多 1 秒 |
| Redis 会丢数据吗 | 会,所以不适合当唯一存储 |
| 高可用怎么做 | 主从 → 哨兵 → Cluster |
| 哨兵故障转移流程 | 主观下线 → 客观下线 → 选 leader → 提 slave |
| Cluster 多少槽 | 16384 |
| MOVED 和 ASK 区别 | MOVED 永久(更新缓存),ASK 迁移中(临时) |
| 跨槽操作怎么办 | 用 hash tag |
| 内存淘汰策略 | 缓存用 allkeys-lru |
| 过期删除策略 | 惰性删除 + 定期删除 |
| 大 key 问题 | 阻塞、倾斜,用 UNLINK 删、拆分 |
| 热 key 问题 | 单节点打爆,本地缓存 + 分散 key |
9.2 追问应对技巧
面试官问「那如果……呢?」时,按这个套路答:
- 承认局限——先说「确实有这个场景」。
- 给方案——说出 1-2 个解决思路。
- 说取舍——每个方案的代价是什么。
- 给结论——实际生产怎么选。
例子:
问:「延迟双删的延迟时间怎么定?」 答:「确实是个问题(承认)。延迟时间要大于读请求『读 DB + 写缓存』的最长耗时(方案)。但这个时间不好精确估计,设短了可能没删干净,设长了影响一致性窗口(取舍)。所以生产上更推荐用 Canal 订阅 binlog 的方式,业务无侵入且不容易漏删(结论)。」
9.3 加分动作
- 主动说取舍——不要只给方案,说出每个方案的缺点。
- 主动做对比——如「Stream 相比 List」「Redis 锁相比 ZooKeeper 锁」。
- 主动关联项目——把话题引到「我在项目里做过类似的事」。
- 主动说边界——如「Redis 不适合当唯一存储」「Stream 不能替代 Kafka」。
9.4 项目话术模板
面试官问「你项目里用过 Redis 吗」,用这个模板:
「用过。我在 XX 项目里用 Redis 做了 XX 功能(如在线状态、消息队列、缓存)。 当时选 Redis 是因为 XX(如需要多机共享状态/需要高性能读)。 中间遇到过 XX 问题(如缓存不一致、锁误删),我用 XX 方案解决了(如 Cache Aside + Lua 脚本)。 如果重来,我可能会用 XX(如 Canal 替代手动删缓存),因为 XX。」
要点:问题 → 方案 → 反思,这三段比单纯说「我用了 Redis」强十倍。
10. 附录:命令速查
String
SET k v / GET k # 设置/获取
SETEX k 10 v # 设值+过期
SETNX k v # 不存在才设
SET k v NX PX 10000 # 分布式锁
INCR k / DECR k / INCRBY k 5 # 计数
MSET/MGET # 批量
APPEND k v / STRLEN k # 追加/长度
Hash
HSET k f v / HGET k f # 设/取字段
HMSET/HMGET # 批量
HGETALL k # 取全部
HINCRBY k f 1 # 字段自增
HDEL k f / HLEN k # 删/数量
HSCAN k 0 # 渐进遍历
List
LPUSH/RPUSH # 插入
LPOP/RPOP # 弹出
BLPOP k 5 / BRPOP k 5 # 阻塞弹出
LRANGE k 0 -1 / LTRIM k 0 9 # 范围/裁剪
LLEN k # 长度
Set
SADD/SREM # 增删
SMEMBERS / SISMEMBER # 全部/判断
SINTER/SUNION/SDIFF # 交并差
SCARD k # 数量
SPOP / SRANDMEMBER # 随机弹出/随机取
ZSet
ZADD k 100 m # 添加
ZSCORE / ZINCRBY # 分值
ZRANGE k 0 -1 WITHSCORES # 升序
ZREVRANGE k 0 9 # 降序取前10
ZRANK / ZREVRANK # 排名
ZRANGEBYSCORE k 90 100 # 分数范围
ZREM k m # 删除
Bitmap / HyperLogLog / GEO
SETBIT k offset 1 / GETBIT k offset
BITCOUNT k / BITOP AND dest k1 k2
PFADD k e1 / PFCOUNT k / PFMERGE
GEOADD k 116.40 39.90 北京
GEODIST k 北京 上海 km
GEORADIUS k 116.40 39.90 10 km
Bitfield
BITFIELD k GET u8 0 # 读8位无符号整数
BITFIELD k SET u8 0 255 # 写8位
BITFIELD k INCRBY u8 0 10 # 自增
BITFIELD k GET u8 0 GET u8 8 # 一次读多个字段(不同偏移)
BITFIELD k SET u8 #1 200 # #1 = 第1个8位字段(自动算偏移)
BITFIELD k OVERFLOW SAT INCRBY u8 0 1 # 饱和溢出(封顶计数)
type 写法:
u无符号 /i有符号 + 位数,如u8、i16、u63(u 最大 63 位,i 最大 64 位) 溢出模式:WRAP(回绕,默认)/SAT(饱和)/FAIL(失败)
Stream
XADD k * f v # 添加
XRANGE k - + # 范围
XGROUP CREATE k g $ # 建组
XREADGROUP GROUP g c STREAMS k > # 组内读
XACK k g id # 确认
XPENDING k g # 查看未确认
XCLAIM k g c 60000 id # 转移
XTRIM k MAXLEN 1000 # 裁剪
Pub/Sub
SUBSCRIBE ch / PSUBSCRIBE news.*
PUBLISH ch msg
PUBSUB CHANNELS
事务 / Lua
MULTI / EXEC / DISCARD
WATCH k / UNWATCH
EVAL "script" 1 key arg
SCRIPT LOAD "script"
EVALSHA sha1 1 key arg
Key 通用
SCAN 0 MATCH user:* COUNT 10 # 渐进遍历(替代KEYS)
DEL k / UNLINK k # 删除/异步删除
EXPIRE k 10 / TTL k # 过期
TYPE k / EXISTS k # 类型/存在
MEMORY USAGE k # 内存占用
OBJECT ENCODING k # 底层编码(调试)
服务端 / 排查
redis-cli --bigkeys # 排查大 key
redis-cli --hotkeys # 排查热 key
redis-cli --latency # 延迟
SLOWLOG GET 10 # 慢查询
INFO / INFO memory / INFO replication
DBSIZE # key 数量
CONFIG SET maxmemory 2gb
常用配置
maxmemory 2gb
maxmemory-policy allkeys-lru # 缓存推荐
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # 混合持久化
save 900 1 # RDB 规则
timeout 300
结语
这份手册的用法:
- 平时:按章读一遍,理解每类怎么用、坑在哪。
- 面试前:只背 第 9 章速查表(30 分钟能过完)。
- 被追问:回第 9.2 节的「追问应对技巧」套路。
三个核心心法:
- 说取舍——给方案时一定带上缺点。
- 做对比——Stream vs List、Redis 锁 vs ZK 锁、Pipeline vs 事务。
- 连项目——用「问题 → 方案 → 反思」模板把八股变成经历。
祝面试顺利。