Redis 专项知识库
面向 Java 后端学习、项目设计与中高级面试。本文按“基础概念 → 数据结构 → 持久化 → 高可用 → 分布式 → 性能优化 → Java 集成 → 场景方案 → 深度面试题”的顺序整理。
目录
- **#1. Redis 总览**
- **#2. Redis 为什么快**
- **#3. Redis 基础命令与通用规则**
- **#4. Redis 数据类型与底层结构**
- **#5. Key 设计与过期策略**
- **AOF**
- **#7. 内存管理与淘汰策略**
- **#8. 事务、Lua 与原子性**
- **#9. 发布订阅、Stream 与消息队列**
- **#10. 主从复制、哨兵与集群**
- **#11. 缓存经典问题与解决方案**
- **#12. 分布式锁与并发控制**
- **#13. Redis 在 Java 中的使用**
- **#14. 常见业务场景设计**
- **#15. 性能诊断与线上排障**
- **#16. 安全与运维**
- **#17. Redis 深度面试题**
- **#18. 速查表**
1. Redis 总览
1.1 Redis 是什么
Redis(Remote Dictionary Server)是一个基于内存的高性能 Key-Value 数据库,常用于:
- 缓存
- 分布式锁
- 计数器
- 排行榜
- 会话存储
- 限流
- 延迟队列
- 消息队列
- 地理位置查询
- 分布式 ID 辅助生成
- 热点数据加速
1.2 Redis 与传统数据库的区别
| 维度 | Redis | MySQL/PostgreSQL 等关系型数据库 |
|---|---|---|
| 存储位置 | 以内存为主,可持久化到磁盘 | 以磁盘为主,依赖 Buffer Pool 缓存 |
| 数据模型 | Key-Value,多种数据结构 | 表、行、列、关系模型 |
| 查询能力 | 按 key 或结构命令访问,不擅长复杂 SQL | 支持复杂 SQL、事务、索引、Join |
| 性能 | 极高,适合低延迟访问 | 更适合强一致、复杂查询和持久存储 |
| 事务能力 | 简单事务,不支持回滚 | ACID 事务完善 |
| 典型定位 | 缓存、并发辅助、实时数据结构 | 主数据源、业务事实存储 |
1.3 Redis 适合与不适合做什么
适合:
- 高频读、低延迟访问
- 数据结构天然适配业务,如 Set 去重、ZSet 排行榜
- 可以接受最终一致性的缓存场景
- 临时状态、短生命周期数据
- 原子计数、限流、锁等并发辅助能力
不适合:
- 作为复杂关系查询的唯一数据库
- 存储超大 Value 或海量冷数据
- 需要严格多表事务的核心账务系统
- 完全替代消息中间件处理复杂 MQ 语义
- 不做持久化与高可用规划就承载关键数据
2. Redis 为什么快
Redis 快不是单一原因,而是多因素叠加:
- 基于内存:大多数操作直接访问内存,避免磁盘随机 IO。
- 高效数据结构:SDS、dict、skiplist、quicklist、listpack 等结构针对常见操作优化。
- 单线程命令执行模型:避免多线程锁竞争和上下文切换,逻辑简单。
- IO 多路复用:使用 epoll/kqueue/select 等机制处理大量连接。
- 命令粒度小:大多数命令时间复杂度低,常见读写为 O(1) 或 O(logN)。
- 协议简单:RESP 协议解析成本低。
- 渐进式 rehash、惰性删除等机制:减少阻塞峰值。
2.1 Redis 真的是单线程吗
需要区分:
- 命令执行主流程:长期以来主要是单线程执行,保证单命令原子性。
- 后台任务:RDB 生成、AOF 重写、异步释放内存、复制等可由子进程或后台线程处理。
- 网络 IO 多线程:较新版本支持多线程处理部分网络读写,但命令执行仍保持核心串行模型。
结论:Redis 不是整个进程只有一个线程,而是核心命令执行路径以单线程串行为主。
2.2 单线程为什么还能高并发
因为 Redis 的瓶颈通常不是 CPU,而是内存访问和网络 IO。单线程配合 IO 多路复用可以在一个线程中管理大量连接事件。只要每个命令足够快,就能达到很高吞吐。
危险点:单线程模型怕慢命令。比如 KEYS *、大 key 删除、超大集合全量遍历、复杂 Lua 脚本都会阻塞后续请求。
3. Redis 基础命令与通用规则
3.1 Key 通用命令
| 命令 | 作用 | 注意点 |
|---|---|---|
EXISTS key | 判断 key 是否存在 | 可同时判断多个 key |
DEL key | 删除 key | 大 key 删除可能阻塞 |
UNLINK key | 异步删除 key | 更适合大 key 删除 |
EXPIRE key seconds | 设置秒级过期 | 返回 1 表示成功 |
PEXPIRE key ms | 设置毫秒级过期 | 精度更高 |
TTL key | 查看剩余秒数 | -1 无过期,-2 不存在 |
PTTL key | 查看剩余毫秒 | 同上 |
TYPE key | 查看数据类型 | 排障常用 |
SCAN cursor | 渐进式遍历 key | 替代 KEYS |
3.2 命令时间复杂度意识
Redis 命令文档都会标注时间复杂度。线上使用必须关注:
- O(1):通常安全,如 GET、SET、HGET。
- O(logN):通常可接受,如 ZADD、ZRANGE 按范围。
- O(N):谨慎,如 LRANGE 大范围、SMEMBERS、HGETALL。
- O(N*M):高危,尤其在大 key 上。
3.3 不推荐的线上命令
| 命令 | 风险 | 替代方案 |
|---|---|---|
KEYS * | 全库扫描,阻塞主线程 | SCAN |
FLUSHALL | 清空全库 | 运维审批、指定 DB、备份 |
HGETALL 大 Hash | 大量返回阻塞网络与主线程 | HSCAN、分页字段设计 |
SMEMBERS 大 Set | 一次性返回全量成员 | SSCAN |
DEL 大 key | 同步释放内存阻塞 | UNLINK |
| 长 Lua 脚本 | 阻塞所有命令 | 拆分、限时、预估复杂度 |
4. Redis 数据类型与底层结构
Redis 的价值不只是 Key-Value,而是“带原子操作的数据结构服务器”。
4.1 String
使用场景
- 缓存对象 JSON
- 计数器
- 分布式 ID 计数
- Session Token
- 短验证码
- Bitmap 的底层载体
常用命令
| 命令 | 作用 |
|---|---|
| `SET key value [EX seconds] [NX | XX]` |
GET key | 获取值 |
MGET k1 k2 | 批量获取 |
INCR key | 自增 |
INCRBY key n | 按指定值自增 |
DECR key | 自减 |
SETNX key value | 不存在才设置,常用于锁基础 |
GETSET key value | 设置新值并返回旧值 |
底层结构 SDS
Redis 没有直接使用 C 字符串,而是使用 SDS(Simple Dynamic String)。优点:
- O(1) 获取字符串长度
- 二进制安全,可以存储任意字节
- 减少缓冲区溢出风险
- 预分配和惰性释放降低频繁扩容成本
面试重点
String 最大 Value 有上限,但实际项目中不应该存超大 Value。超大 JSON 会导致网络传输慢、序列化慢、内存碎片严重、更新粒度粗。
4.2 Hash
使用场景
- 用户信息对象
- 商品库存局部字段
- 配置项
- 小对象缓存
常用命令
| 命令 | 作用 |
|---|---|
HSET key field value | 设置字段 |
HGET key field | 获取字段 |
HMGET key f1 f2 | 批量获取字段 |
HINCRBY key field n | 字段自增 |
HDEL key field | 删除字段 |
HLEN key | 字段数量 |
HSCAN key cursor | 渐进遍历字段 |
String JSON vs Hash
| 维度 | String JSON | Hash |
|---|---|---|
| 读完整对象 | 简单 | 需要取多个字段或 HGETALL |
| 更新局部字段 | 需要反序列化整体写回 | 可直接 HSET 单字段 |
| 序列化成本 | 高 | 低 |
| 结构演进 | 灵活但缺少字段级操作 | 字段清晰 |
| 内存 | 小对象 Hash 可能更省 | 大量 field 也会有开销 |
建议:完整对象读多写少用 String JSON;字段频繁局部更新用 Hash。
4.3 List
使用场景
- 简单队列
- 最新列表
- 消息缓冲
- 时间线片段
常用命令
| 命令 | 作用 |
|---|---|
LPUSH/RPUSH | 左/右插入 |
LPOP/RPOP | 左/右弹出 |
BLPOP/BRPOP | 阻塞弹出 |
LRANGE key start stop | 范围读取 |
LTRIM key start stop | 裁剪列表 |
底层结构 quicklist
Redis List 早期使用 ziplist + linkedlist,后来演进为 quicklist:多个紧凑列表节点组成双向链表,兼顾内存紧凑与插入删除效率。
注意点
List 可以做简单队列,但缺少可靠消息确认、重试、死信等能力。可靠队列更适合 Redis Stream 或专业 MQ。
4.4 Set
使用场景
- 去重
- 标签集合
- 共同好友
- 抽奖
- 黑名单/白名单
- 点赞用户集合
常用命令
| 命令 | 作用 |
|---|---|
SADD key member | 添加成员 |
SREM key member | 删除成员 |
SISMEMBER key member | 判断是否存在 |
SCARD key | 成员数量 |
SINTER | 交集 |
SUNION | 并集 |
SDIFF | 差集 |
SRANDMEMBER | 随机取成员 |
SPOP | 随机弹出 |
底层结构
- 整数集合:成员全是整数且数量较小时使用 intset。
- 哈希表:通用场景使用 dict。
4.5 Sorted Set / ZSet
使用场景
- 排行榜
- 延迟队列
- 权重排序
- 热度榜
- 时间线排序
- 范围检索
常用命令
| 命令 | 作用 |
|---|---|
ZADD key score member | 添加或更新分数 |
ZSCORE key member | 获取分数 |
ZRANGE key start stop [WITHSCORES] | 正序排名 |
ZREVRANGE | 倒序排名 |
ZRANGEBYSCORE | 按分数范围查 |
ZREM | 删除成员 |
ZCARD | 成员数量 |
ZINCRBY | 分数自增 |
ZRANK/ZREVRANK | 获取排名 |
底层结构
ZSet 通常由:
- dict:member → score,支持 O(1) 查分数。
- skiplist:按 score 排序,支持范围查询和排名。
小数据量时可能使用 listpack 编码以节省内存。
跳表为什么适合 Redis
跳表比红黑树更容易实现范围遍历,插入删除逻辑简单,性能期望 O(logN),适合 ZSet 的排名和范围查询。
4.6 Bitmap
Bitmap 本质是 String 的 bit 位操作。
使用场景
- 用户签到
- 活跃用户统计
- 布尔状态记录
- 大规模开关标记
常用命令
| 命令 | 作用 |
|---|---|
SETBIT key offset value | 设置 bit |
GETBIT key offset | 获取 bit |
BITCOUNT key | 统计 1 的数量 |
BITOP AND/OR/XOR/NOT | 位运算 |
BITFIELD | 按位域读写 |
示例:签到
key:sign:user:1001:2026-05
- 第 1 天 offset = 0
- 第 2 天 offset = 1
- 签到:
SETBIT sign:user:1001:2026-05 11 1 - 统计:
BITCOUNT sign:user:1001:2026-05
4.7 HyperLogLog
使用场景
- UV 统计
- 大规模去重计数
特点
- 极省内存。
- 统计结果是近似值,有误差。
- 不保存具体元素,不能反查成员。
命令:
PFADD key elementPFCOUNT keyPFMERGE dest src1 src2
4.8 GEO
GEO 用于地理位置存储和附近查询,底层基于 ZSet。
常用命令:
GEOADDGEOPOSGEODISTGEOSEARCH
场景:附近商家、附近车辆、门店距离排序。
4.9 Stream
Stream 是 Redis 提供的日志型消息结构。
核心概念
| 概念 | 说明 |
|---|---|
| Stream | 消息流 |
| Entry | 一条消息,包含 ID 和字段 |
| Consumer Group | 消费者组 |
| Consumer | 组内消费者 |
| PEL | Pending Entries List,待确认消息列表 |
| ACK | 消费成功确认 |
常用命令
| 命令 | 作用 |
|---|---|
XADD | 添加消息 |
XREAD | 读取消息 |
XGROUP CREATE | 创建消费者组 |
XREADGROUP | 消费者组读取 |
XACK | 确认消息 |
XPENDING | 查看待确认消息 |
XCLAIM | 转移长时间未确认消息 |
XTRIM | 裁剪流长度 |
Stream 与专业 MQ 对比
Redis Stream 适合轻量消息流和缓存体系内部异步任务,但如果需要复杂路由、事务消息、海量堆积、严格消息治理,RocketMQ/Kafka/RabbitMQ 更合适。
5. Key 设计与过期策略
5.1 Key 命名规范
推荐格式:
业务:模块:实体:标识:属性示例:
user:profile:1001
order:detail:202605120001
shop:goods:list:shopId:10:page:1
lock:order:pay:202605120001
rate:user:1001:api:sendSms原则:
- 可读性强。
- 避免过长。
- 避免特殊字符混乱。
- 同类 key 保持一致。
- 集群场景需要考虑 hash tag,如
order:{1001}:detail。
5.2 过期时间设计
过期时间不是随便设,要根据数据类型设计:
| 数据 | 推荐 TTL |
|---|---|
| 验证码 | 1-5 分钟 |
| 登录 token | 与登录态策略一致 |
| 热点商品 | 数分钟到数小时,可随机扰动 |
| 字典配置 | 较长 TTL + 主动更新 |
| 分布式锁 | 必须设置短 TTL,防死锁 |
| 限流 key | 与窗口周期一致 |
5.3 TTL 随机化
为避免大量 key 同时过期导致缓存雪崩,可以给 TTL 加随机扰动:
TTL = 基础时间 + random(0, 扰动时间)例如商品缓存 30 分钟,可设置 30-35 分钟随机过期。
6. 持久化机制 RDB/AOF
Redis 是内存数据库,但支持持久化。持久化解决“重启后数据恢复”问题,不等于强一致落盘。
6.1 RDB
RDB 是某个时间点的内存快照。
触发方式
- 手动
SAVE:阻塞主线程,不推荐线上使用。 - 手动
BGSAVE:fork 子进程生成快照。 - 配置规则自动触发。
- 主从复制初次同步时触发。
- 关闭 Redis 时可能触发。
优点
- 文件紧凑,适合备份。
- 恢复速度相对快。
- 对主线程影响较小,因为主要由子进程写文件。
缺点
- 可能丢失最近一次快照后的数据。
- fork 子进程时可能有内存和延迟开销。
- 大内存实例生成快照压力大。
6.2 AOF
AOF 记录写命令日志,通过重放命令恢复数据。
appendfsync 策略
| 策略 | 含义 | 风险 |
|---|---|---|
| always | 每次写都刷盘 | 最安全但最慢 |
| everysec | 每秒刷盘 | 常用,最多丢约 1 秒数据 |
| no | 交给操作系统 | 性能高但丢失风险大 |
AOF 重写
AOF 会越来越大,Redis 会重写 AOF:将当前数据状态转换成最小命令集合,减少文件体积。
6.3 RDB 与 AOF 对比
| 维度 | RDB | AOF |
|---|---|---|
| 文件内容 | 数据快照 | 写命令日志 |
| 恢复速度 | 较快 | 取决于日志大小 |
| 数据安全 | 可能丢较多 | everysec 下最多约 1 秒 |
| 文件大小 | 小 | 通常更大 |
| 适合 | 备份、灾难恢复 | 尽量减少数据丢失 |
6.4 混合持久化
混合持久化将 RDB 快照和 AOF 增量日志结合:前半部分是 RDB,后半部分是增量 AOF。优点是恢复快且丢失较少。
6.5 fork 与 Copy-On-Write
RDB/AOF 重写常用 fork 子进程。fork 后父子进程共享内存页;当父进程写入数据时,操作系统复制被修改的页,这就是 COW。写入越频繁,COW 内存开销越大。
7. 内存管理与淘汰策略
7.1 过期删除策略
Redis 删除过期 key 主要有两种方式:
- 惰性删除:访问 key 时检查是否过期,过期则删除。
- 定期删除:后台周期性抽样检查并删除过期 key。
为什么不定时删除每个 key?因为为每个 key 创建定时器成本太高。
7.2 内存淘汰策略
当内存达到 maxmemory 后,Redis 按策略淘汰 key。
| 策略 | 说明 |
|---|---|
noeviction | 不淘汰,写入报错 |
allkeys-lru | 从所有 key 中淘汰最近最少使用 |
volatile-lru | 从设置过期时间的 key 中淘汰 LRU |
allkeys-random | 从所有 key 中随机淘汰 |
volatile-random | 从设置过期时间的 key 中随机淘汰 |
volatile-ttl | 淘汰即将过期的 key |
allkeys-lfu | 从所有 key 中淘汰最不常用 |
volatile-lfu | 从设置过期时间的 key 中淘汰 LFU |
缓存场景常用 allkeys-lru 或 allkeys-lfu。如果 Redis 里混有不能丢的数据,要慎重配置。
7.3 LRU 与 LFU
- LRU:最近最少使用,看最近访问时间。
- LFU:最不经常使用,看访问频率。
LRU 对偶发扫描污染敏感;LFU 更适合长期热点识别。
7.4 大 key 问题
大 key 指 Value 很大或集合元素过多。
风险:
- 单命令耗时长,阻塞主线程。
- 网络传输慢。
- 删除释放内存慢。
- 迁移、持久化、复制压力大。
- 集群数据倾斜。
排查:
redis-cli --bigkeysMEMORY USAGE keySCAN+ 类型检测- 慢日志与监控
治理:
- 拆分 key。
- 分页读取。
- 使用
UNLINK异步删除。 - 控制集合长度,如
LTRIM、XTRIM。 - 业务上限制单对象大小。
7.5 热 key 问题
热 key 是访问极高的 key,可能导致单节点 CPU 或网络打满。
解决:
- 本地缓存 Caffeine。
- 热 key 拆分副本,如
hot:goods:1:0~N。 - 读写分离。
- CDN/边缘缓存。
- 请求合并与互斥重建。
- 热点探测与动态扩散。
8. 事务、Lua 与原子性
8.1 Redis 事务
Redis 事务使用:
MULTI
命令1
命令2
EXEC特点:
- 命令入队后统一执行。
- 执行期间不会插入其他客户端命令。
- 不支持传统数据库事务回滚。
- 命令执行失败不会自动回滚之前命令。
8.2 WATCH 乐观锁
WATCH key 可以监控 key,如果事务执行前 key 被其他客户端修改,则 EXEC 失败。
适合 CAS 场景,但高并发下重试成本可能较高。
8.3 Lua 脚本
Lua 脚本在 Redis 中原子执行,适合多命令组合。
优点:
- 减少网络往返。
- 多步骤逻辑原子化。
- 避免并发穿插。
注意:
- 脚本不能太慢。
- 不要在脚本中遍历大 key。
- 集群中脚本访问的 key 通常需要落在同一 slot。
- 需要用
KEYS[]传 key,用ARGV[]传参数。
8.4 原子性边界
Redis 单条命令原子,Lua 脚本整体原子,但不代表业务端到端原子。例如“扣库存 + 写数据库 + 发消息”不能只靠 Redis 原子性保证全局一致。
9. 发布订阅、Stream 与消息队列
9.1 Pub/Sub
发布订阅命令:
PUBLISH channel messageSUBSCRIBE channelPSUBSCRIBE pattern
特点:
- 实时广播。
- 不持久化消息。
- 消费者离线会丢消息。
- 不适合可靠消息队列。
9.2 List 队列
使用 LPUSH + BRPOP 可实现阻塞队列。
问题:
- 消息被 pop 后如果消费者宕机,消息丢失。
- 缺少 ACK、重试、死信。
可用 RPOPLPUSH 或 BRPOPLPUSH 做处理中队列,但复杂度上升。
9.3 Stream 队列
Stream 支持消息 ID、消费者组、ACK、Pending 列表,更适合可靠消费。
但 Redis Stream 仍不是所有场景都替代 Kafka/RocketMQ。消息量巨大、长时间堆积、复杂路由和事务消息建议使用专业 MQ。
10. 主从复制、哨兵与集群
10.1 主从复制
Redis 支持一主多从。
用途:
- 读写分离。
- 数据冗余。
- 支撑哨兵故障转移。
复制过程:
- 从节点连接主节点。
- 主节点生成 RDB 快照并发送给从节点。
- 从节点加载 RDB。
- 主节点持续发送复制缓冲区中的增量命令。
- 后续保持命令流同步。
10.2 全量复制与部分复制
- 全量复制:传输完整 RDB,成本高。
- 部分复制:根据复制偏移量和 repl backlog 传输缺失命令。
如果从节点断开太久,backlog 覆盖了缺失数据,就只能全量复制。
10.3 复制延迟
原因:
- 主节点写入过高。
- 网络延迟。
- 从节点机器性能差。
- 从节点正在执行慢命令。
- 大 key 或大批量写。
影响:读从库可能读到旧数据。强一致读应读主库或设计读写一致性策略。
10.4 哨兵 Sentinel
Sentinel 负责监控、通知、自动故障转移。
核心功能:
- 监控主从节点状态。
- 主观下线和客观下线判断。
- 选举 Leader Sentinel。
- 选择新主节点。
- 通知客户端新主地址。
主观下线:单个 Sentinel 认为主节点不可用。
客观下线:多个 Sentinel 达成 quorum 后确认主节点不可用。
10.5 Redis Cluster
Cluster 用于数据分片和高可用。
Slot 机制
Redis Cluster 有 16384 个 hash slot。key 通过 CRC16 计算落到某个 slot,slot 分布在不同主节点。
hash tag
使用 {} 可让多个 key 落到同一个 slot:
order:{1001}:detail
order:{1001}:items这对多 key 操作、Lua 脚本很重要。
MOVED 与 ASK
MOVED:slot 已迁移到别的节点,客户端应更新路由缓存。ASK:slot 正在迁移,客户端临时访问目标节点。
10.6 Cluster 限制
- 多 key 操作要求 key 在同一 slot。
- 数据倾斜会导致某些节点压力大。
- 大 key 迁移成本高。
- 客户端必须支持 Cluster 协议。
11. 缓存经典问题与解决方案
11.1 缓存穿透
问题
查询一个数据库也不存在的数据,缓存中没有,每次请求都打到数据库。
解决方案
- 缓存空值:数据库查不到也写入空标记,设置短 TTL。
- 布隆过滤器:判断请求 key 是否可能存在,不存在直接拒绝。
- 参数校验:非法 ID、异常参数直接拦截。
- 风控限流:对恶意请求限流。
注意
缓存空值要设置较短 TTL,否则新数据创建后可能短时间读不到。
11.2 缓存击穿
问题
热点 key 过期瞬间,大量请求同时打到数据库。
解决方案
- 互斥锁重建缓存:只有一个线程查库并回填,其余等待或返回旧值。
- 逻辑过期:缓存不物理过期,后台异步刷新。
- 热点 key 永不过期 + 主动更新。
- 预热与续期。
11.3 缓存雪崩
问题
大量 key 同时过期,或 Redis 整体不可用,导致请求集中打到数据库。
解决方案
- TTL 加随机值。
- 多级缓存。
- Redis 高可用集群。
- 限流、降级、熔断。
- 缓存预热。
- 关键数据逻辑过期。
11.4 缓存与数据库一致性
常见策略:
| 策略 | 说明 | 风险 |
|---|---|---|
| 先更新 DB 再删缓存 | 常用 | 删除失败会脏读 |
| 先删缓存再更新 DB | 不推荐 | 并发下旧值可能回填缓存 |
| 更新 DB 后更新缓存 | 写复杂,容易乱序 | 并发覆盖风险 |
| 延迟双删 | 先删缓存,更新 DB,延迟再删 | 经验型方案,不是强保证 |
| Binlog 订阅删缓存 | 通过 Canal 等监听 DB 变更 | 架构复杂 |
推荐基础方案:先更新数据库,再删除缓存。如果删除失败,使用重试队列、消息表、binlog 订阅或定时校验补偿。
11.5 为什么是删除缓存而不是更新缓存
因为缓存通常是复杂查询结果或聚合结果。直接更新容易遗漏关联缓存,并发下可能旧请求后写覆盖新值。删除后让下一次读重新构建,更简单可靠。
11.6 双写一致性深度分析
场景:线程 A 更新 DB,线程 B 读取。
推荐流程:
- 写请求更新 DB。
- 写请求删除缓存。
- 读请求缓存未命中,查 DB 回填。
极端风险:删除缓存失败,或主从数据库延迟导致回填旧值。解决靠补偿、读主库、延迟删除、版本号校验。
12. 分布式锁与并发控制
12.1 分布式锁基本要求
一个合格分布式锁至少要满足:
- 互斥性:同一时刻只有一个客户端持有锁。
- 防死锁:客户端宕机后锁能自动释放。
- 不误删:只能释放自己的锁。
- 可重入性:视业务需要。
- 自动续期:长任务需要。
- 高可用:Redis 故障时有策略。
12.2 SET NX EX
基础加锁:
SET lock:order:1 randomValue NX EX 30释放锁必须比较 value 后删除,不能直接 DEL:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end12.3 为什么 value 要唯一
如果客户端 A 获得锁后超时,锁自动释放;客户端 B 获得锁。此时 A 恢复后如果直接 DEL,会删掉 B 的锁。唯一 value 用于确认“这是我的锁”。
12.4 锁过期时间如何设置
锁 TTL 应大于业务正常执行时间,但不能无限长。长任务需要续期机制。Redisson 的 watchdog 会在锁未释放时自动续期。
12.5 Redisson
Redisson 提供:
- 可重入锁
- 公平锁
- 读写锁
- 联锁
- 红锁
- 信号量
- 分布式集合
- watchdog 自动续期
12.6 RedLock 争议
RedLock 试图在多个独立 Redis 节点上获取多数锁,提高容错。争议在于网络分区、时钟漂移、客户端停顿等情况下是否足够安全。
实践建议:
- 普通业务互斥可用单 Redis/哨兵/集群 + 唯一值 + Lua 释放。
- 极高一致性场景不要只依赖 Redis 锁,应考虑数据库事务、唯一约束、ZooKeeper/etcd、幂等和状态机。
12.7 分布式锁不等于幂等
锁只能降低并发冲突,不能替代幂等。支付、扣库存、订单状态流转必须依赖业务唯一键、状态机、数据库约束等最终兜底。
13. Redis 在 Java 中的使用
13.1 常见 Java 客户端
| 客户端 | 特点 |
|---|---|
| Jedis | 同步阻塞,API 接近 Redis 命令,早期常用 |
| Lettuce | 基于 Netty,线程安全,支持同步/异步/响应式 |
| Redisson | 提供分布式对象和锁,抽象更高 |
| Spring Data Redis | Spring 生态封装,常配合 RedisTemplate 使用 |
Spring Boot 默认常用 Lettuce。
13.2 RedisTemplate 与 StringRedisTemplate
RedisTemplate<Object, Object>:可自定义序列化,适合对象操作。StringRedisTemplate:key/value 都按字符串处理,适合简单字符串和 JSON。
13.3 序列化选择
| 序列化 | 优点 | 缺点 |
|---|---|---|
| JDK 序列化 | 默认可用 | 可读性差、体积大、跨语言差 |
| String | 简单直观 | 需要自行处理对象 |
| JSON | 可读、跨语言 | 体积略大、类型处理要规范 |
| Kryo/Protostuff | 性能好、体积小 | 引入复杂度、兼容性要管理 |
建议:业务缓存常用 String key + JSON value,避免 JDK 默认序列化带来的乱码 key 和迁移问题。
13.4 Spring Cache 集成
常用注解:
| 注解 | 作用 |
|---|---|
@Cacheable | 查询时缓存结果 |
@CachePut | 执行方法并更新缓存 |
@CacheEvict | 删除缓存 |
@Caching | 组合多个缓存操作 |
注意:
- 自调用不会触发代理。
- key 设计要明确。
- TTL 需要配置。
- 复杂一致性场景不要盲目依赖注解。
13.5 Pipeline
Pipeline 将多个命令一次性发送,减少网络 RTT。
适合:
- 批量写入。
- 批量查询。
- 初始化缓存。
注意:Pipeline 不是事务,不保证中间不插入其他命令。
13.6 连接池与超时
关键配置:
- 连接超时。
- 读写超时。
- 最大连接数。
- 最大空闲连接。
- 最小空闲连接。
- 命令超时。
线上必须设置超时,不能无限等待 Redis,避免线程池被拖死。
13.7 Java 缓存 Aside 模式伪代码
public ProductDTO getProduct(Long id) {
String key = "product:detail:" + id;
String json = redis.get(key);
if (json != null) {
if ("__NULL__".equals(json)) {
return null;
}
return JSON.parseObject(json, ProductDTO.class);
}
Product product = productMapper.selectById(id);
if (product == null) {
redis.setex(key, 60, "__NULL__");
return null;
}
redis.setex(key, 1800 + random.nextInt(300), JSON.toJSONString(product));
return convert(product);
}13.8 Java 分布式锁伪代码
String key = "lock:order:" + orderId;
String value = UUID.randomUUID().toString();
boolean locked = redis.set(key, value, NX, EX, 30);
if (!locked) {
throw new BusyException();
}
try {
// business
} finally {
// use Lua: compare value then delete
unlockByLua(key, value);
}14. 常见业务场景设计
14.1 短信验证码
设计:
sms:code:{phone} -> code, TTL 5min
sms:limit:{phone} -> count, TTL 1min要点:
- 验证码短 TTL。
- 发送频率限制。
- 校验失败次数限制。
- 成功后删除验证码。
- 防止手机号枚举和短信轰炸。
14.2 用户签到
使用 Bitmap:
sign:user:{userId}:{yyyyMM}- 签到:
SETBIT key day-1 1 - 当月签到次数:
BITCOUNT key - 连续签到:从当天向前扫描 bit。
14.3 排行榜
使用 ZSet:
rank:score:{biz}:{period}- 加分:
ZINCRBY rank 10 userId - TopN:
ZREVRANGE rank 0 99 WITHSCORES - 用户排名:
ZREVRANK rank userId
要点:
- 周榜/月榜分 key。
- 历史榜单设置合理 TTL 或归档。
- 分数相同时可组合时间权重。
14.4 限流
固定窗口计数器
rate:{api}:{userId}:{yyyyMMddHHmm}INCR 后首次设置过期。
优点简单;缺点是窗口边界可能突刺。
滑动窗口
使用 ZSet 记录请求时间戳:
- 删除窗口外记录。
- 统计窗口内数量。
- 未超限则写入当前请求。
- 设置 TTL。
适合精确限流,但成本比固定窗口高。
14.5 延迟队列
使用 ZSet:
delay:queue:orderCancel
score = executeTimestamp
member = taskId/orderId消费者轮询 ZRANGEBYSCORE now,抢到后 ZREM,再执行业务。
风险:
- 轮询延迟。
- 消费失败需重试。
- 多消费者要确保抢占原子性,可用 Lua。
14.6 秒杀库存
Redis 常用于秒杀预扣库存:
- 商品库存预热到 Redis。
- Lua 原子判断库存和用户是否重复购买。
- 成功后写入消息队列异步下单。
- 数据库最终扣减,唯一索引防重复。
关键:Redis 只是前置削峰,最终一致性和账务正确性必须由数据库约束、消息可靠性和补偿机制保证。
14.7 购物车
可用 Hash:
cart:{userId}
field = skuId
value = count 或 JSON优点:局部增删改方便。
14.8 关注关系
使用 Set:
followings:{userId}
followers:{userId}- 关注:两个 Set 同步写。
- 共同关注:
SINTER followings:A followings:B
注意大 V 粉丝 Set 可能成为大 key,需要拆分或落库为主。
14.9 Feed 流
- 推模式:用户发动态时推到粉丝收件箱,读快写慢。
- 拉模式:读时拉取关注用户动态,写快读慢。
- 推拉结合:普通用户推,大 V 拉。
Redis 可存时间线索引,正文落 DB/对象存储。
14.10 布隆过滤器
用于缓存穿透防护。特点:
- 判断不存在一定不存在。
- 判断存在可能误判。
- 不能直接删除普通元素。
可用 RedisBloom 模块或应用本地布隆过滤器。
15. 性能诊断与线上排障
15.1 慢查询
命令:
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET注意:慢查询统计的是命令执行时间,不包含网络传输和排队时间。
15.2 latency monitor
用于分析 Redis 内部延迟事件:
LATENCY DOCTOR
LATENCY LATEST15.3 INFO 关键指标
常看模块:
| 模块 | 关注点 |
|---|---|
| clients | 连接数、阻塞客户端 |
| memory | used_memory、碎片率、evicted_keys |
| persistence | rdb/aof 状态 |
| stats | ops、hits、misses、rejected_connections |
| replication | 主从延迟、复制状态 |
| cpu | CPU 使用 |
| cluster | 集群状态 |
| keyspace | key 数量和过期数量 |
15.4 缓存命中率
命中率:
keyspace_hits / (keyspace_hits + keyspace_misses)命中率低可能原因:
- TTL 太短。
- key 设计不一致。
- 缓存预热不足。
- 访问模式不适合缓存。
- 大量不存在请求穿透。
15.5 Redis 变慢排查顺序
- 查看慢日志。
- 查看 CPU 是否打满。
- 查看网络流量和连接数。
- 检查是否有大 key、热 key。
- 查看内存和淘汰情况。
- 查看 fork、RDB/AOF 重写。
- 查看主从延迟。
- 查看客户端连接池耗尽或超时。
- 检查是否使用了 KEYS/HGETALL/SMEMBERS 等危险命令。
15.6 常见线上故障
| 故障 | 可能原因 | 处理 |
|---|---|---|
| Redis CPU 高 | 热 key、慢命令、Lua 复杂 | 查 slowlog、热 key 拆分 |
| 内存暴涨 | 大 key、TTL 缺失、缓存污染 | bigkeys、设置 TTL、拆分 |
| 频繁淘汰 | maxmemory 太小、策略不当 | 扩容、调整策略 |
| 连接数满 | 连接泄漏、池配置不当 | 查 clients、修复连接池 |
| 主从延迟大 | 写入高峰、大 key、网络慢 | 限流、拆分、扩容 |
| AOF 重写卡顿 | fork/COW 压力 | 调整重写时机、扩容内存 |
16. 安全与运维
16.1 安全配置
- 不要暴露公网。
- 设置密码/ACL。
- 禁用或重命名危险命令。
- 使用最小权限账号。
- 开启 TLS(如环境需要)。
- 绑定内网地址。
- 配置防火墙和安全组。
16.2 备份策略
- 定期 RDB 备份。
- 备份文件异地存储。
- 定期演练恢复。
- 核心数据不能只放 Redis。
16.3 容量规划
需要考虑:
- 原始数据大小。
- Redis 对象开销。
- 过期 key 比例。
- 内存碎片。
- fork COW 额外内存。
- 主从复制 backlog。
- 峰值写入。
16.4 上线检查清单
- [ ] 是否设置
maxmemory。 - [ ] 是否选择合适淘汰策略。
- [ ] 是否设置密码/ACL。
- [ ] 是否禁用危险命令。
- [ ] 是否配置持久化。
- [ ] 是否配置主从/哨兵/集群。
- [ ] 是否有监控和告警。
- [ ] 是否存在大 key。
- [ ] 是否存在无 TTL 的缓存 key。
- [ ] 客户端是否设置超时和连接池。
- [ ] 是否有降级方案。
17. Redis 深度面试题
17.1 Redis 为什么快?只答“因为内存”够吗?
不够。Redis 快的原因包括:基于内存、高效数据结构、单线程避免锁竞争、IO 多路复用、RESP 协议简单、命令复杂度低、后台任务与主线程分离等。真正面试时要强调:Redis 快的前提是命令不能太慢,一旦出现大 key、慢 Lua、全量扫描,单线程模型会放大阻塞影响。
17.2 Redis 单线程为什么还能支持高并发?
Redis 使用 IO 多路复用在一个线程中处理大量连接事件,命令执行通常是内存操作,耗时极短。单线程避免了多线程锁竞争和上下文切换。高并发不等于并行执行命令,而是事件处理效率高、单命令足够快。
17.3 Redis 真的只有一个线程吗?
不是。核心命令执行路径主要是单线程,但 RDB、AOF 重写、异步释放、网络 IO 的部分处理、复制等可能使用子进程或后台线程。面试应区分“命令执行单线程”和“Redis 进程只有一个线程”这两个概念。
17.4 Redis 事务为什么不支持回滚?
Redis 事务更像批量命令队列。它保证 EXEC 执行时命令连续执行,但不提供关系型数据库那种回滚机制。原因是 Redis 命令失败多属于编程错误,且回滚会增加复杂度和性能成本。业务上需要自己保证命令正确性,或用 Lua 做原子校验。
17.5 Redis 的过期 key 是如何删除的?
惰性删除 + 定期删除。访问 key 时发现过期会删除;后台周期性抽样检查过期 key 并删除。这样避免为每个 key 建定时器的高成本,同时也避免只靠惰性删除导致过期 key 长期占内存。
17.6 Redis 内存满了怎么办?
如果配置 maxmemory,达到上限后根据淘汰策略处理:noeviction 直接拒绝写入,LRU/LFU/random/TTL 等策略会淘汰 key。线上缓存通常选择 allkeys-lru 或 allkeys-lfu。但如果 Redis 中存放不可丢数据,就不能随意淘汰,必须隔离实例或调整设计。
17.7 LRU 和 LFU 有什么区别?
LRU 看最近是否访问,适合时间局部性强的场景;LFU 看访问频率,更能识别长期热点。LRU 容易被偶发批量扫描污染,LFU 对稳定热点更友好。
17.8 RDB 和 AOF 的区别?怎么选?
RDB 是快照,文件小、恢复快,但可能丢失最近一次快照后的数据。AOF 是写命令日志,数据安全性更好,但文件更大、恢复可能更慢。一般生产可使用 AOF everysec 或混合持久化,同时保留 RDB 做备份。具体选择取决于数据丢失容忍度和性能要求。
17.9 AOF 重写会阻塞 Redis 吗?
AOF 重写主要由子进程完成,不会全程阻塞主线程。但 fork 子进程、COW 内存复制、磁盘 IO 压力可能造成延迟抖动。大内存实例在重写期间如果写入很高,COW 开销会明显增加。
17.10 什么是缓存穿透、击穿、雪崩?
- 穿透:查不存在的数据,每次都打 DB。解决:空值缓存、布隆过滤器、参数校验。
- 击穿:热点 key 过期瞬间大量请求打 DB。解决:互斥重建、逻辑过期、热点永不过期。
- 雪崩:大量 key 同时失效或 Redis 整体不可用。解决:TTL 随机化、高可用、限流降级、多级缓存。
17.11 如何保证缓存与数据库一致性?
常用策略是先更新数据库,再删除缓存。删除失败要有补偿机制,如重试队列、消息表、binlog 监听。不要轻易更新缓存,因为复杂查询缓存容易漏更新,并发下可能旧值覆盖新值。强一致场景不能只靠缓存策略,要以数据库事务和约束为准。
17.12 为什么不推荐“先删缓存再更新数据库”?
因为并发下可能出现:线程 A 删除缓存后准备更新 DB;线程 B 读缓存未命中,查到旧 DB 并回填缓存;线程 A 再更新 DB。最终缓存中是旧值,直到过期才恢复。
17.13 延迟双删能彻底解决一致性吗?
不能。延迟双删是降低旧值回填概率的经验方案,不是严格一致性证明。延迟时间很难精确设置,受数据库事务时间、主从延迟、业务执行时间影响。关键业务应使用补偿、版本号、binlog 订阅或直接读主库。
17.14 Redis 分布式锁怎样才安全?
基础要求:SET key value NX EX seconds 原子加锁;value 必须唯一;释放锁用 Lua 比较 value 后删除;锁必须有过期时间;长任务需要续期;业务必须幂等。不能使用 SETNX 后单独 EXPIRE 的非原子组合,也不能直接 DEL 释放。
17.15 Redisson watchdog 是什么?
Redisson 获取锁后,如果没有显式指定 leaseTime,会启动 watchdog 自动续期,默认周期性延长锁过期时间,防止业务未执行完锁就过期。业务完成释放锁后续期停止。如果服务宕机,watchdog 停止,锁最终过期释放。
17.16 RedLock 是否一定安全?
不能说一定安全。RedLock 通过多个独立 Redis 节点多数派加锁提高可用性,但在网络分区、进程长暂停、时钟漂移等场景仍有争议。高价值强一致资源建议使用数据库约束、状态机、ZooKeeper/etcd 或业务幂等兜底。
17.17 Big Key 会造成什么问题?
大 key 会导致单命令耗时长、网络传输慢、删除阻塞、持久化和复制压力增大、集群迁移困难、内存倾斜。治理方式是拆分、分页读取、限制集合长度、使用 UNLINK 异步删除、避免一次性 HGETALL/SMEMBERS。
17.18 Hot Key 如何发现和解决?
发现方式包括 Redis 监控、客户端埋点、代理层统计、slowlog、网络流量分析、redis-cli --hotkeys(需合适淘汰策略支持)。解决方式包括本地缓存、key 副本拆分、读写分离、热点预热、请求合并、限流降级。
17.19 Redis Cluster 为什么是 16384 个 slot?
Redis Cluster 通过 16384 个 hash slot 做分片。slot 数量在路由表大小、迁移粒度和管理成本之间折中。客户端只需维护 slot 到节点的映射。key 通过 CRC16 对 16384 取模定位 slot。
17.20 Cluster 中多 key 操作有什么限制?
多 key 操作要求所有 key 位于同一个 slot,否则会报 CROSSSLOT。可以使用 hash tag,如 order:{1001}:detail 和 order:{1001}:items,让 {1001} 参与 hash,从而落到同一 slot。
17.21 主从复制会不会丢数据?
可能。Redis 主从复制默认异步,主节点写成功后还没同步给从节点就宕机,故障转移后这部分数据可能丢失。可以通过 min-replicas-to-write 等配置降低风险,但会牺牲可用性和写入成功率。
17.22 哨兵如何判断主节点下线?
先由单个 Sentinel 判断主观下线;当足够数量 Sentinel 都认为主节点不可用,达到 quorum,形成客观下线。随后 Sentinel 选举 Leader,由 Leader 执行故障转移,选择一个从节点晋升为主节点。
17.23 为什么从库读可能读到旧数据?
主从复制是异步的,从库存在复制延迟。写入主库后立即读从库,可能读不到最新数据。解决方式:关键链路读主库、写后短时间读主、使用版本号判断、监控复制延迟并动态切换。
17.24 Pipeline 和事务有什么区别?
Pipeline 是客户端批量发送命令,减少网络往返,不保证原子性。事务是 Redis 服务端 MULTI/EXEC 命令队列,EXEC 时连续执行,具备一定原子执行边界。Pipeline 可以包含事务命令,但二者概念不同。
17.25 Lua 和事务有什么区别?
Lua 可以在服务端执行复杂逻辑,并且整个脚本原子执行,还能根据中间结果决定后续命令。事务只是命令排队,不能在 EXEC 前基于前一条命令结果动态分支。复杂原子校验通常优先用 Lua。
17.26 ZSet 为什么同时用 dict 和 skiplist?
dict 用于 O(1) 根据 member 查询 score;skiplist 用于按 score 排序、范围查询和排名。两者组合同时满足快速查分和有序范围访问。如果只用跳表,按 member 查找不够快;如果只用 dict,无法高效范围排序。
17.27 跳表相比红黑树有什么优势?
跳表实现简单,范围遍历自然,插入删除不需要复杂旋转,平均 O(logN)。Redis 作者选择跳表主要是工程实现和范围查询便利性的折中,并不是红黑树性能不行。
17.28 Redis Hash 什么时候比 String JSON 更合适?
当对象字段需要频繁局部更新、局部读取、计数字段自增时,Hash 更合适。String JSON 适合整体读写、结构灵活的对象缓存。若对象很大却只改一个字段,String JSON 会带来反序列化和全量写回成本。
17.29 Redis 可以做消息队列吗?
可以,但要看可靠性要求。List 可做简单队列但可靠性弱;Pub/Sub 不持久化,消费者离线会丢消息;Stream 支持消费者组、ACK 和 Pending,更接近消息队列。但复杂消息治理、大规模堆积和事务消息仍建议专业 MQ。
17.30 Stream 的 Pending List 有什么作用?
消费者组读取消息后,未 ACK 的消息会进入 PEL。通过 XPENDING 可查看待确认消息,通过 XCLAIM 可将超时未确认消息转移给其他消费者处理。这使 Redis Stream 具备一定消息可靠性和故障恢复能力。
17.31 Redis 为什么不能执行很慢的 Lua?
Lua 脚本执行期间 Redis 主线程被占用,其他命令无法执行。长脚本会造成全局阻塞,引发超时。脚本必须控制复杂度,避免大 key 遍历和不确定循环。
17.32 如何设计一个高并发商品详情缓存?
方案:商品详情使用 Cache Aside;热点商品预热;TTL 加随机扰动;空值缓存防穿透;互斥锁或逻辑过期防击穿;本地缓存承接极热读;更新商品时先更新 DB 再删缓存;删除失败进入重试;监控命中率、重建耗时和 DB 压力。
17.33 如何设计秒杀扣库存?
预热库存到 Redis;使用 Lua 原子判断库存、用户是否重复购买、扣减库存;成功后写入 MQ 异步创建订单;数据库用唯一索引防重复下单,用乐观锁或条件扣减保证库存不超卖;失败消息要补偿;Redis 库存和 DB 最终对账。
17.34 如果 Redis 宕机,系统如何降级?
按业务重要性分级:可读 DB 兜底但限流;热点页面返回本地缓存或静态降级;写链路绕过缓存直接写 DB;关闭非核心功能;启用熔断防止 DB 被打爆;恢复后预热缓存。不能让所有请求无保护地穿透到 DB。
17.35 如何避免缓存 key 被恶意打爆?
参数校验、认证鉴权、限流、布隆过滤器、空值缓存、业务 ID 范围约束、异常访问监控。缓存层不应成为非法请求放大器。
17.36 Redis 和本地缓存如何配合?
本地缓存如 Caffeine 延迟更低,适合极热点和变化不频繁的数据;Redis 作为分布式共享缓存。组合时要解决本地缓存失效通知,可用消息广播、短 TTL、版本号。风险是多实例本地缓存不一致。
17.37 什么是缓存预热?
系统启动或活动开始前,把热点数据提前加载进缓存,避免冷启动时大量请求打到 DB。预热数据来源可以是历史访问排行、运营配置、活动商品列表。预热后还要设置合理 TTL 和刷新策略。
17.38 什么是缓存污染?
大量低价值、只访问一次的数据进入缓存,占用空间并挤出热点数据。解决:只缓存高价值数据、设置较短 TTL、使用 LFU、按业务分实例或分命名空间、控制批量扫描类请求。
17.39 Redis 连接池耗尽怎么排查?
看应用线程是否大量阻塞在 Redis 调用;检查连接是否泄漏;查看 Redis CLIENT LIST;检查慢命令导致连接长期占用;核对连接池最大连接、等待时间、命令超时;确认网络是否抖动。修复时要设置合理超时并优化慢命令。
17.40 如何处理 Redis 中的无 TTL 缓存 key?
先区分业务语义:确实需要长期存在的数据和遗漏 TTL 的缓存。对缓存 key 应补充 TTL;对核心持久状态应考虑是否应该放 DB。可以通过 SCAN 抽样、命名规范和监控发现无 TTL key。
17.41 Redis 的 DEL 和 UNLINK 有什么区别?
DEL 同步删除并释放内存,大 key 可能阻塞。UNLINK 先从 keyspace 解除关联,后台异步释放内存,更适合删除大 key。但它不是所有场景都能替代 DEL,核心区别在释放内存是否异步。
17.42 SCAN 能完全替代 KEYS 吗?
线上遍历应使用 SCAN,因为它渐进式返回,不会一次阻塞全库。但 SCAN 不保证一次遍历期间结果是强一致快照,可能出现重复或遗漏,需要客户端去重或容忍。运维离线场景才考虑 KEYS。
17.43 Redis 为什么会出现内存碎片?
频繁分配和释放不同大小对象会造成 allocator 碎片;大 key 更新、过期删除、AOF/RDB 期间 COW 也会影响内存表现。可通过 mem_fragmentation_ratio 观察,必要时开启主动碎片整理或重启迁移。
17.44 Redis 如何做附近的人/附近商家?
使用 GEO。写入经纬度和 member;查询时用 GEOSEARCH 按半径或矩形范围查找,并返回距离。底层是将经纬度编码为 geohash 后存入 ZSet。注意国内地图坐标系转换和距离排序精度。
17.45 Redis 如何实现排行榜同分按时间排序?
可将 score 设计为复合分数,例如主分数放大后叠加时间因子;或者 ZSet score 存主排序值,member 中带时间并在业务层二次排序。要注意浮点精度和分数范围。严谨场景可用两个结构辅助排序。
17.46 Redis 中存对象应该注意什么?
避免过大对象;明确序列化格式;key 和字段命名稳定;设置 TTL;处理版本兼容;避免把敏感信息无加密放入缓存;更新时考虑一致性;不要依赖缓存作为唯一事实来源。
17.47 什么情况下 Redis 不应该做缓存?
数据访问频率低、命中率低、强一致要求极高、数据体积巨大、查询结果高度个性化且难复用、维护缓存成本超过收益时,不适合缓存。缓存不是越多越好。
17.48 Redis Cluster 扩容时会发生什么?
新增节点后需要迁移一部分 slot 到新节点。迁移期间客户端可能收到 MOVED/ASK 重定向。大 key 会拖慢迁移,迁移过程中要关注延迟、失败重试和客户端兼容性。
17.49 如何保证 Redis 操作和数据库操作的一致提交?
Redis 和数据库之间没有天然分布式事务。通常通过最终一致性解决:数据库本地事务提交后发送消息/记录 outbox,再异步删除或更新缓存;失败可重试。强一致要求应把核心状态放数据库,用 Redis 做派生缓存。
17.50 面试中如何回答“Redis 用过哪些场景”?
不要只罗列“缓存、锁、队列”。应按场景讲清楚:业务背景、key 设计、数据结构选择、TTL、并发一致性、异常处理、监控指标。例如“商品详情缓存使用 String JSON + 随机 TTL + 空值缓存 + 互斥重建 + DB 更新后删缓存 + 删除失败重试”。
18. 速查表
18.1 数据类型选型
| 需求 | 推荐类型 |
|---|---|
| 普通对象缓存 | String / Hash |
| 局部字段更新 | Hash |
| 计数器 | String INCR |
| 去重集合 | Set |
| 排行榜 | ZSet |
| 延迟队列 | ZSet / Stream |
| 签到 | Bitmap |
| UV 近似统计 | HyperLogLog |
| 附近位置 | GEO |
| 轻量可靠消息 | Stream |
18.2 缓存问题速记
| 问题 | 核心原因 | 常用方案 |
|---|---|---|
| 穿透 | 查不存在数据 | 空值缓存、布隆过滤器 |
| 击穿 | 热点 key 过期 | 互斥锁、逻辑过期 |
| 雪崩 | 大量 key 失效/Redis 故障 | TTL 随机、高可用、降级 |
| 不一致 | DB 与缓存双写 | 先更新 DB 再删缓存 + 补偿 |
| 大 key | value/集合过大 | 拆分、分页、UNLINK |
| 热 key | 单 key 访问过高 | 本地缓存、副本拆分、限流 |
18.3 生产禁忌
- 不在线上使用
KEYS *扫全库。 - 不对大集合使用
HGETALL/SMEMBERS/LRANGE 0 -1。 - 不使用无 TTL 的缓存 key。
- 不直接
DEL释放别人的锁。 - 不把 Redis 当强一致主数据库。
- 不在 Lua 中写不可控循环。
- 不把 Redis 暴露到公网。
- 不让缓存故障无保护地打穿数据库。
18.4 高分面试回答公式
回答 Redis 问题时可按以下结构:
概念定义 → 底层原理 → 典型场景 → 风险边界 → 生产方案 → 监控/排障例如回答分布式锁:
Redis 锁用于跨 JVM 互斥;底层依赖 SET NX EX 原子加锁;释放用 Lua 比较 value 再删除;长任务用 Redisson watchdog 续期;但锁不等于幂等,最终要用数据库唯一约束和状态机兜底;监控锁等待时间、失败率和业务超时。19. 推荐学习路线
- 先掌握 String、Hash、List、Set、ZSet 的命令和场景。
- 理解 SDS、dict、skiplist、quicklist 等底层结构。
- 掌握过期删除、内存淘汰、RDB/AOF。
- 掌握缓存穿透、击穿、雪崩和一致性。
- 学会 Java 中 RedisTemplate、Lettuce、Redisson 的使用边界。
- 理解主从、哨兵、Cluster 和 slot。
- 通过真实项目练习缓存设计、分布式锁、限流、排行榜、延迟队列。
- 最后训练面试表达:不要背八股,要能讲清楚取舍、风险和兜底。
20. 面试前自检清单
- [ ] 能解释 Redis 为什么快,并说出单线程边界。
- [ ] 能区分 RDB、AOF、混合持久化。
- [ ] 能说清楚过期删除和内存淘汰。
- [ ] 能设计安全的分布式锁。
- [ ] 能处理缓存穿透、击穿、雪崩。
- [ ] 能解释缓存与 DB 一致性方案。
- [ ] 能分析大 key、热 key、慢查询。
- [ ] 能说明 ZSet 底层为什么用跳表。
- [ ] 能描述主从复制、哨兵、Cluster。
- [ ] 能结合 Java 项目讲 RedisTemplate/Redisson 实战。