Redis 速查手册
速查向 · 数据结构与底层编码、持久化、高可用、缓存三大问题、分布式锁、常见问题解析
1. 概述与基础
1.1 Redis 为什么快
Q:Redis 单线程为什么还这么快?
纯内存操作(无磁盘 IO);单线程无锁无上下文切换与竞争开销;IO 多路复用(epoll)单线程管理大量连接;高效数据结构(跳表、压缩列表、渐进式 rehash)。
注意:Redis 6.0 之后网络 IO 是多线程的,但命令执行仍是单线程。
注意:Redis 6.0 之后网络 IO 是多线程的,但命令执行仍是单线程。
Q:单线程的瓶颈在哪?
CPU 不是瓶颈,网络带宽和内存才是。单线程下,大 key、慢命令(
keys *、hgetall 大 hash)会阻塞所有请求。
1.2 与 Memcached 对比
- 数据类型:Redis 支持 String/List/Hash/Set/ZSet 等;Memcached 只有 KV。
- 持久化:Redis 支持 RDB/AOF;Memcached 无。
- 多线程:Memcached 多线程,Redis 命令执行单线程。
- Value 上限:Redis 512MB;Memcached 1MB。
1.3 应用场景
| 场景 | 说明 |
|---|---|
| 缓存 | 热点数据、页面缓存、会话(Session) |
| 计数器 / 限流 | INCR、滑动窗口、令牌桶 |
| 排行榜 | ZSet 有序集合 |
| 分布式锁 | SET NX EX + Lua 释放 |
| 消息队列 | List(LPUSH/BRPOP)、Stream |
| 社交关系 | Set 交集并集(共同关注) |
| BitMap / 签到 | SETBIT、BITCOUNT,签到统计、布隆过滤器 |
2. 数据类型与底层编码
2.1 五大基础类型
| 类型 | 特点 | 典型用途 |
|---|---|---|
| String | 二进制安全,可存任意数据 | 缓存对象、计数器、分布式锁 |
| List | 双向链表,两端操作 O(1) | 消息队列、最新列表 |
| Hash | field-value 映射 | 存对象、购物车 |
| Set | 无序去重集合 | 标签、共同好友、去重 |
| ZSet | 有序集合,按 score 排序 | 排行榜、延迟队列 |
2.2 底层编码(高频考点)
| 类型 | 编码 | 转换条件 |
|---|---|---|
| String | int / embstr / raw | 整数值→int;≤44 字节→embstr;否则 raw |
| List | listpack / quicklist | 3.2+ 统一用 quicklist(listpack 节点组成的双向链表) |
| Hash | listpack / hashtable | 元素 < 512 且值 < 64 字节→listpack,否则 hashtable |
| Set | intset / listpack / hashtable | 全整数且元素少→intset;非整数小集合→listpack |
| ZSet | listpack / skiplist | 元素 < 128 且值 < 64 字节→listpack,否则 skiplist |
阈值可配:
hash-max-listpack-entries、zset-max-listpack-entries 等(旧版本叫 *-ziplist-*)。转换单向不可逆。2.3 跳跃表 skiplist
- 多层链表,上层是下层的"快速通道",查找平均 O(logN),实现比平衡树简单。
- 每层晋升概率 25%,层高上限 32。
- ZSet 同时用字典 + 跳表:字典保证 O(1) 查 score,跳表保证范围查询 O(logN)。
2.4 特殊类型
- BitMap:String 的位操作,
SETBIT/SETBIT/BITCOUNT/BITOP,用于签到、活跃统计。 - HyperLogLog:基数统计(UV),固定 12KB,误差 0.81%。
- GEO:基于 ZSet 存经纬度,附近的人。
- Stream:消息队列,支持消费组、ACK、持久化。
3. 持久化
3.1 RDB vs AOF
| 维度 | RDB | AOF |
|---|---|---|
| 内容 | 某一时刻的数据快照(二进制) | 记录的写命令日志 |
| 恢复速度 | 快 | 慢(需重放命令) |
| 文件体积 | 小(压缩) | 大 |
| 数据安全 | 差(可能丢最后一次快照后的数据) | 好(取决于 appendfsync) |
| 对性能影响 | fork 子进程,有瞬时阻塞 | 写盘有 IO 开销 |
RDB 触发:save(阻塞)/ bgsave(fork 子进程,写时复制 COW)。
AOF 刷盘策略(appendfsync):
always:每条命令都 fsync,最安全、最慢。everysec:每秒 fsync,默认,最多丢 1 秒数据。no:交给 OS,最快、最不安全。
AOF 重写:命令越来越多导致文件膨胀,用
bgrewriteaof fork 子进程压缩重写为最小命令集。重写期间新命令写入 AOF 重写缓冲区,完成后追加。3.2 混合持久化
Redis 4.0+ 支持 aof-use-rdb-preamble yes:AOF 文件前半部分是 RDB 格式的全量快照,后半部分是增量命令。重启恢复又快又安全。
选择:纯缓存可只用 RDB;要求数据可靠用 AOF everysec 或混合持久化。
4. 高可用与集群
4.1 主从复制
- 全量同步:从节点首次连接,主节点
bgsave生成 RDB 发送,期间新命令写入 repl_buffer 补齐。 - 增量同步:基于 repl_backlog_buffer(环形缓冲区)+ offset 续传。
- psync 命令:从节点带 runid + offset 请求,主节点判断能否增量(backlog 是否覆盖)。
- 复制风暴:多从节点同时全量同步会打爆主节点——用"主→从→从"级联。
4.2 哨兵 Sentinel
- 监控主从节点,主节点宕机时自动故障转移(选新主、通知客户端)。
- 主观下线(单个哨兵认为挂了)→ 客观下线(超过 quorum 个哨兵认同)。
- 选举 Leader 哨兵执行故障转移,基于 Raft 思想。
- 选主规则:优先级 → offset 最大 → runid 最小。
4.3 Cluster 分片集群
- 16384 个哈希槽,
CRC16(key) % 16384决定 key 归属槽。 - 每个节点负责一部分槽,节点间用 Gossip 协议通信。
- 至少 3 主 3 从(6 节点)才能保证高可用。
- 数据倾斜可用 hash tag:
{user1000}.following保证同槽。 - 为什么是 16384?——心跳包用 bitmap 存槽,2KB 大小(16384 bit)够用,65536 则要 8KB,浪费带宽。
5. 缓存三大问题(必考)
5.1 缓存穿透
Q:什么是缓存穿透?怎么解决?
查询不存在的数据,缓存和 DB 都没有,请求每次都打到 DB(常见于恶意攻击)。
解决:① 缓存空值(设短过期时间);② 布隆过滤器 前置拦截;③ 参数校验,拦截非法请求。
解决:① 缓存空值(设短过期时间);② 布隆过滤器 前置拦截;③ 参数校验,拦截非法请求。
5.2 缓存击穿
Q:什么是缓存击穿?怎么解决?
热点 key 突然过期,大量并发请求瞬间压到 DB。
解决:① 互斥锁(只有一个线程去加载,其他等待);② 逻辑过期(key 不过期,value 存逻辑过期时间,异步更新);③ 热点数据永不过期 + 后台刷新。
解决:① 互斥锁(只有一个线程去加载,其他等待);② 逻辑过期(key 不过期,value 存逻辑过期时间,异步更新);③ 热点数据永不过期 + 后台刷新。
5.3 缓存雪崩
Q:什么是缓存雪崩?怎么解决?
大量 key 同时过期 或 Redis 集群宕机,请求全部涌向 DB,DB 被打挂。
解决:① 过期时间加随机值打散;② 多级缓存(本地 + Redis);③ 熔断限流降级;④ Redis 高可用(主从+哨兵/集群)。
解决:① 过期时间加随机值打散;② 多级缓存(本地 + Redis);③ 熔断限流降级;④ Redis 高可用(主从+哨兵/集群)。
5.4 缓存一致性
Q:如何保证缓存与数据库的一致性?
推荐:先更新 DB,再删除缓存(Cache Aside)。
延迟双删:更新 DB 前删缓存 → 更新 DB → 延迟再删一次。
更强的方案:Canal 订阅 binlog 异步删缓存,或分布式锁串行化。
注意:删除比更新好(避免并发写导致脏数据);写成"更新 DB + 更新缓存"在并发下更易不一致。
延迟双删:更新 DB 前删缓存 → 更新 DB → 延迟再删一次。
更强的方案:Canal 订阅 binlog 异步删缓存,或分布式锁串行化。
注意:删除比更新好(避免并发写导致脏数据);写成"更新 DB + 更新缓存"在并发下更易不一致。
为什么"先删缓存再更新 DB"不行:并发下 A 删缓存 → B 读旧值回填缓存 → A 更新 DB,缓存里留了旧数据。
6. 分布式锁
6.1 正确实现
- 加锁:
SET key value NX EX 30——原子操作,value 用唯一标识(UUID+线程ID)。 - 释放:用 Lua 脚本保证"判断是自己的锁 + 删除"原子执行,避免误删他人的锁。
- 自动续期:业务未完成锁就到期 → Redisson 看门狗(watchdog)每 1/3 过期时间续期。
错误写法:
SETNX + EXPIRE 两条命令不是原子的(中间宕机则死锁);直接 DEL 会误删别人的锁。6.2 Redlock 争议
- Redlock 需向 N/2+1 个独立 Redis 实例加锁,多数成功且耗时小于锁有效期才算成功。
- 争议:Martin Kleppmann 认为它无法保证强一致(时钟漂移、GC 停顿),不如用 fencing token;Antirez 反驳。
- 生产上更常见的是单实例/主从 + Redisson,或直接用 ZooKeeper/etcd。
7. 内存与淘汰
7.1 过期删除策略
- 惰性删除:访问 key 时才检查是否过期(省 CPU,占内存)。
- 定期删除:每 100ms 随机抽查一部分 key 删除(默认 20 个/库,超 25% 继续)。
- Redis 是两者结合。
7.2 内存淘汰策略(maxmemory-policy)
| 策略 | 说明 |
|---|---|
| noeviction | 默认,不淘汰,写入报错 |
| allkeys-lru | 所有 key 中淘汰最近最少用(缓存常用) |
| allkeys-lfu | 所有 key 中淘汰使用频率最低(4.0+) |
| allkeys-random | 随机淘汰 |
| volatile-lru / lfu / random / ttl | 只在设置了过期时间的 key 中淘汰 |
LRU 用近似算法(采样 N 个 key,选最久未用的),避免维护完整链表。
8. 速查清单
| 主题 | 一句话答案 |
|---|---|
| 为什么快 | 内存 + 单线程无锁 + IO 多路复用 + 高效结构 |
| Redis 6 多线程 | 只有网络 IO 多线程,命令执行仍单线程 |
| RDB vs AOF | 快照(快、可能丢) vs 命令日志(安全、慢) |
| 混合持久化 | RDB 头 + AOF 尾,4.0+ |
| ZSet 底层 | 元素少用 listpack,多则 skiplist + dict |
| Cluster 槽位 | 16384 个,CRC16 % 16384 |
| 穿透 | 查不存在数据 → 缓存空值 / 布隆过滤器 |
| 击穿 | 热点 key 过期 → 互斥锁 / 逻辑过期 |
| 雪崩 | 大量 key 同时过期 → 随机过期时间 + 高可用 |
| 一致性 | 先更新 DB 再删缓存 + 延迟双删 |
| 分布式锁 | SET NX EX + Lua 释放 + 看门狗续期 |
| 淘汰策略 | 默认 noeviction;缓存推荐 allkeys-lru |