Redis 底层原理与知识要点
本文总结 Redis 的核心底层原理:数据结构实现、单线程模型、持久化机制、过期与淘汰策略、缓存三大问题、高可用方案以及常见问题排查要点,作为个人技术笔记。
五种基础数据结构与底层实现
string:SDS
string 底层是 SDS(Simple Dynamic String),而非 C 字符串:
- 带长度字段,获取长度 O(1)
- 预分配空间 + 惰性释放,减少内存重分配次数
- 二进制安全,可存任意二进制数据
- 3.2+ 分
sdshdr5/8/16/32/64按长度分级,节省内存
list:双向链表与 quicklist
- 3.2 之前:元素少且小时用 ziplist(压缩列表),否则用双向链表
- 3.2+ 统一为 quicklist:以 ziplist 为节点组成的双向链表,兼顾内存紧凑与读写效率
- 7.0 之后引入 listpack 逐步替代 ziplist(解决连锁更新问题)
quicklist 本质上就是”链表 + 压缩块”,通过 list-max-ziplist-size 控制每个节点的大小。
hash:哈希表与 ziplist
- 元素少且小时:ziplist
- 元素多时:hashtable,rehash 采用渐进式 rehash(分多次搬迁,避免一次性阻塞)
set:整数集合与哈希表
- 全部为整数且数量少时:intset(整数集合),有序紧凑存储,查找二分
- 否则:hashtable(值为 null)
zset:跳表 + 哈希表
zset 底层是跳表(skiplist) + 哈希表的组合:
- 哈希表存 member -> score,O(1) 取分数
- 跳表按 score 排序,支持范围查询、排名(zrange/zrank)O(logN)
- 元素少时也用 ziplist/listpack 压缩存储
跳表相比平衡树实现简单、区间遍历友好,Redis 作者因此选跳表实现有序集合。
单线程模型与 IO 多路复用
为什么单线程还快
- 基于内存操作,瓶颈不在 CPU
- 核心是 IO 多路复用(epoll):单线程注册多个 socket 事件,有事件才处理,无事件阻塞,避免线程切换与锁竞争
- 数据结构高效,命令执行本身微秒级
事件循环流程:
1 | |
Redis 6.0 多线程
- 6.0 引入多线程 IO:网络读写(accept/read/write)由多个线程并行处理,命令执行仍是单线程
- 解决的是大流量下网络 IO 成为瓶颈的问题,而非 CPU 计算瓶颈
- 默认关闭,通过
io-threads开启(建议 4 核以下不开启)
Redis 单线程意味着一个慢命令会阻塞所有客户端,生产环境禁止大 key 操作、keys *、hgetall 大 hash、lrange 大 list。
持久化
RDB 快照
- 全量快照:
save(同步阻塞)/bgsave(fork 子进程后台生成 dump.rdb) - 触发条件:
save 900 1 300 10 60 10000等配置,或shutdown、主从全量同步 - 优点:文件紧凑、加载快、适合备份;缺点:两次快照之间数据会丢
AOF 日志
- 追加写命令,三种写回策略(appendfsync):
| 策略 | 说明 | 数据安全性 |
|---|---|---|
| always | 每次写命令 fsync 磁盘 | 最安全,性能最差 |
| everysec | 每秒 fsync(默认) | 最多丢 1 秒数据 |
| no | 交给操作系统刷盘 | 可能丢较多数据 |
- AOF 文件膨胀后触发
bgrewriteaof重写压缩
混合持久化(4.0+)
aof-use-rdb-preamble yes:AOF 重写时先写 RDB 格式的全量数据,再追加增量命令- 优点:加载快(RDB 部分直接加载)+ 丢失数据少
fork 与 copy-on-write
bgsave/bgrewriteaof 都依赖 fork + COW:
- fork 创建子进程,子进程共享父进程内存页表
- 主进程继续写时触发写时复制,被修改的页复制一份
- 内存越大、写越频繁,fork 耗时与 COW 内存开销越大
内存过大(如超过物理内存一半)时 bgsave 可能因 COW 触发 OOM;fork 耗时在 1ms 以上需要关注。
过期删除与内存淘汰
过期删除策略
- 惰性删除:访问 key 时才检查是否过期,过期则删。节省 CPU,但过期 key 堆积占内存
- 定期删除:每秒采样删除部分过期 key(默认 10 次/秒,每次取 20 个样本)
- 两者配合使用,平衡 CPU 与内存
内存淘汰策略(8 种)
达到 maxmemory 时触发,8 种策略:
1 | |
缓存场景建议 allkeys-lru;需要区分热点与非热点的用 allkeys-lfu;不能丢数据的业务绝不依赖淘汰兜底。
缓存三大问题与解决方案
缓存穿透
现象:查询一个不存在的 key,缓存无数据,每次打到 DB。
解决方案:
- 缓存空值(key 不存在也缓存空串 + 短过期时间)
- 布隆过滤器(Bloom Filter):前置拦截不存在的 key
- 接口层参数校验(如 id 非法的直接拒绝)
缓存击穿
现象:某个热点 key 过期的瞬间,大量请求同时打到 DB。
解决方案:
- 互斥锁(分布式锁):重建缓存的请求加锁,其余等待
- 逻辑过期:key 永不过期,后台线程异步刷新
- 热点 key 过期时间加随机值错开
缓存雪崩
现象:大量 key 同一时间过期,或 Redis 宕机,DB 被打垮。
解决方案:
- 过期时间加随机抖动:
setex key random(60~120s) - 多级缓存(本地缓存 Caffeine + Redis)
- Redis 高可用(哨兵/集群)+ 限流降级兜底
缓存击穿是”单个热点 key”,雪崩是”大面积 key 同时失效”,定位时要区分清楚。
高可用架构
主从复制
- 全量同步:从节点发
psync ? -1,主节点 bgsave 生成 RDB 传给从节点,期间写入记录在 repl backlog 中,同步完成后补发增量 - 增量同步:断线重连后基于偏移量从 backlog 增量同步
- 从节点默认只读,
slaveof指定主从关系
哨兵 Sentinel
- 作用:监控主从节点、自动故障转移、通知客户端
- 判定:主观下线(单个哨兵超时)→ 客观下线(多数哨兵确认)→ 选举新主(从节点优先级、复制偏移量、run_id)
- 哨兵自身集群部署,至少 3 个实例
集群 Cluster
- 16384 个槽位,key 通过
CRC16(key) % 16384决定归属节点 - 客户端访问错节点时返回 MOVED 重定向(槽位迁移中返回 ASK),客户端需处理重定向
- 数据分片 + 主从副本,
cluster meet组建集群 - 槽位迁移、多 key 操作(mget)跨槽不支持
Redis 分布式锁
1 | |
- 红锁(RedLock):多节点加锁,过半成功才算成功,用于强一致场景
- Redisson 看门狗:默认 30s 锁超时,后台定时续期(默认每 10s 续一次),业务未完成时防止锁被自动释放;宕机时看门狗停更,锁自然过期释放
Redis 分布式锁适合大多数业务,但要清楚它基于 AP 架构;对一致性要求极高的场景考虑 ZooKeeper 锁。
常规问题排查
内存暴涨
redis-cli --bigkeys扫描大 key(大 hash/zset/string)memory usage key查看单个 key 内存info memory查看 used_memory、fragmentation 等指标- 常见原因:无过期时间的 key 堆积、大 value、list 无限追加(消息堆积)、内存碎片(
activedefrag yes)
慢查询
slowlog get 10查看慢命令slowlog-log-slower-than阈值配置(默认 10000 微秒)- 定位后优化:拆小 key、避免 O(N) 命令(
hgetall/lrange大范围)、keys换scan
CPU 高 / Hot Key
hotkeys参数开启后redis-cli --hotkeys检测(4.0+)- 缓解:本地缓存热 key、读写分离(从节点分担读)、key 打散(
key#1~`key#N` 分片到不同节点) - 注意:高频小 key 的 CPU 消耗主要在协议解析与内存分配,可评估
io-threads
数据不一致(双写一致性)
常见方案:
- Cache Aside:先更新 DB,再删缓存;删缓存失败用重试/订阅 binlog(Canal)+ MQ 补偿
- 保证最终一致:更新 DB 成功 → 延迟双删(先删缓存,延迟 500ms 再删一次)
- 强一致场景:读写都走 DB 或分布式事务,缓存只做加速
“先删缓存再更新 DB”在并发读下极易产生脏数据,生产应统一”先更 DB 再删缓存”。
持久化阻塞与延迟抖动
- 大实例 bgsave fork 阻塞主线程:控制单实例内存(建议 10GB 内),或换用物理机大内存 +
fork前避免大写入 - AOF
always策略磁盘慢导致抖动:评估改everysec - 触发场景排查:
info stats看latest_fork_usec,检查是否有频繁 bgsave(如主从全量重连) - 内存碎片导致延迟:
info memory看mem_fragmentation_ratio > 1.5时开启activedefrag - 网络抖动:客户端连接池 + 合理的超时重试策略
小结
- 底层数据结构都是”小数据压缩、大数据换结构”的策略(ziplist/quicklist/intset → 跳表/哈希表)
- 单线程 + epoll 是高吞吐的根基,任何 O(N) 操作都是性能雷区
- 持久化、过期淘汰、缓存问题、高可用本质都是对”内存、磁盘、网络”三者的权衡
- 排查问题的核心抓手:
info、slowlog、--bigkeys/--hotkeys、memory usage