【Redis深入】一、快的原因
1. 为什么快
- 纯内存操作,💡 简单命令微秒级响应(纳秒级是 CPU Cache 访问级别,Redis GET 实际约 50-100μs,避免被挑战)
- 命令执行单线程,无锁、无上下文切换
- 自始至终使用 epoll/kqueue 实现 I/O 多路复用,单线程高效处理万级连接
- Redis 6.0+ 可选开启多线程 I/O,仅并行化网络数据的读写和协议解析,命令执行仍为单线程
- 💡 紧凑编码体系 :小对象自动采用
int、embstr、listpack、intset 等紧凑编码,大量小 key/value 不分配独立堆内存,减少 malloc/free 系统调用,同时提升 CPU Cache Line 命中率(这是比"纯内存"更深层的性能倍增器)
- 💡 渐进式摊还保障 P99 延迟 :Dict 渐进式 Rehash、大 Key 惰性删除(
UNLINK + lazy free)、过期键惰性清理,将 O(N) 重操作摊还到每次 O(1) CRUD 中。"快"不仅是吞吐高,更是长尾延迟稳定
- 💡 RESP 协议 + Pipeline:协议极简解析零拷贝;Pipeline 将 N 次 RTT 压缩为 1 次,吞吐量提升数十倍(端到端性能不可或缺的一环)
2. 数据结构
2.1 SDS
- SDS(Simple Dynamic String),数据结构里维护了数组长度及数组,预分配空间不够时才用新对象替换旧对象,否则直接在原对象空闲区追加
- 数据库内核操作(协议解析、二进制存储、内存管理、持久化)使"获取字符串长度"成为最高频基础操作 → C 字符串 O(N) 长度计算成为瓶颈且不支持二进制 → SDS 通过显式 len 字段 + 预分配机制,同时解决 O(1) 长度、二进制安全和频繁重分配三个问题
- 💡 补充 embstr 编码:短字符串(≤44字节 in Redis 7.0)使用 embstr,SDS header + 内容在一次 malloc 中分配,内存连续、释放仅需一次 free,是短 key/value 内存效率极高的关键
2.2 跳表
- 跳表本质是随机多层级链表,工程实现中通常限制最高 64 层
- 插入数据:先执行查询,从当前最高层逐层向下定位,得到 update\[\] 数组(记录每层插入位置前驱节点)。随后通过概率 p=0.25 逐层抛硬币决定新节点最高层级(底层必定存在),最后在 update\[\] 对应层级执行链表指针变更
- 删除数据:同样先查询填充 update\[\] 定位目标节点,在各层逐一解除指针链接。若删除后最高层变空,需同步降低跳表当前最高层级
- 基于概率分布(大数定律),高层节点数量呈等比数列衰减,形成天然近似平衡索引结构,查找、插入、删除期望时间复杂度均稳定在 O(logN)
- 💡 为什么选跳表而非红黑树/B+树? ① 实现简单,代码可维护性高;② 范围查询(ZRANGEBYSCORE)只需找到起点后沿底层链表顺序遍历,O(logN + M),而红黑树需中序遍历效率低;③ 并发修改时局部锁粒度更小(虽然 Redis 单线程不需要,但为未来扩展留余地)
2.3 dict
- Redis 在内存中维护两个 dict
- 扩容时只需将另一个 dict 大小设为原来 2 倍,标记 rehashIndex,通过主线程 CRUD 操作逐步迁移数据。Redis 7.0+ 引入后台线程加速纯数据搬运,但扩缩容触发判断始终由主线程独占
- 💡 修正扩容触发条件:并非"键值对数量 >= hash桶数量"就扩容。默认 load factor ≥ 1 触发扩容;但在 BGSAVE/BGREWRITEAOF 期间,为避免 COW 导致内存翻倍,阈值放宽至 load factor ≥ 5 才扩容;缩容在 load factor < 0.1 时触发
- 读写一致性保证:
- Rehash 过程中,查找同时查两个 dict
- 新增直接写入新 dict
- 删除两个 dict 都检查并删除
- 遍历同时遍历两个 dict
- 💡 补充渐进式 Rehash 的本质:将集中式 O(N) 耗时摊还到每次 O(1) 操作中,是保证延迟稳定的核心思想,而非仅仅"后台搬运"
3. 存储类型
- String 使用 SDS / 💡 int / embstr
- Hash 使用 dict / 💡 listpack(小对象)
- Set 使用 dict / 💡 intset / listpack(小对象)
- ZSet 使用 dict + skiplist / 💡 listpack(小对象)
- List 使用 quicklist(💡 双向链表 + listpack 混合结构)
3.1 String
- String 把整块字符串塞到 value 中,key 是键值,用于缓存、计数器、Session
- 💡 分布式锁深化 :不仅利用单线程原子性,还需明确:①
SET key value NX PX ttl 一条命令完成加锁+过期;② value 必须是 UUID/随机串防误删;③ WatchDog 续期解决业务未完成锁过期问题;④ 集群下 RedLock 的争议与适用边界
3.2 Hash
- Hash 相对 String 可存储整体对象,每个字段对应一个 dict
| 层级 |
解决的问题 |
独立String能否替代 |
| 网络层 |
局部读写,最小化传输字节 |
✅ 能(拆key) |
| 内存层 |
紧凑编码,大幅降低内存开销 |
❌ 不能(多key元数据爆炸) |
| 语义层 |
对象完整性、批量快照、安全删除 |
⚠️ 勉强(需应用层额外维护) |
| 计算层 |
字段级原子运算,卸载客户端逻辑 |
❌ 不能(无对应原语) |
3.3 Set
- Set 用于去重、标签、好友关系
- 💡 补充底层分层:纯整数小集合用 intset(排序数组+二分查找),非整数小集合用 listpack,大集合才升级为 hashtable。这与 Hash/ZSet 的分层逻辑一致,是 Redis 内存效率的核心设计
3.4 ZSet
- 在 Set 基础上引入 score 字段,支持排序和范围查找,用于排行榜
- score 设为时间戳时可作延迟队列(客户端轮询时间戳范围内数据)和滑动窗口限流
- 💡 补充紧凑编码:元素少且值短时使用 listpack(Redis 7.0+)或 ziplist(旧版),超出阈值才升级为 dict + skiplist。面试不提此点会被认为只懂理论不懂工程优化
3.5 List
- 利用 List 两端取数据能力
- 消息队列:FIFO/LIFO 语义 + 💡 BRPOP/BLPOP 原生阻塞等待(Hash/ZSet 不具备)
- 最新消息:LPUSH + LRANGE + 💡 LTRIM 自动裁剪(既是容量保护又是淘汰策略)
- 分页:LRANGE start stop
- 💡 深分页陷阱:LRANGE 是 O(N+M),跳到第 1000 页需从头遍历 20000 元素。List 仅适合浅分页,深度分页应改用 ZSet 或游标分页
- 💡 QuickList 本质:不是单纯链表,而是双向链表 + listpack 混合结构,既保留 O(1) 两端操作,又利用紧凑结构的缓存局部性
4、过期策略
- Redis可以给每个键设置过期时间TTL(Time To Live)
- 但是删除是惰性删除+定期删除
- 惰性:访问时检查,保证正确性,兜底冷数据。
- 定期:每100ms随机抽样,过期率>25%则追加轮次,实现清理强度与过期密度的自适应平衡。
- 本质:CPU 与内存的权衡,放弃精确排序,选择概率近似。
5、淘汰策略
- LRU(Least Recently Used),淘汰最久未被访问的数据。关注的是"时间",认为最近用过的将来还会用。
- LFU(Least Frequently Used),淘汰访问次数最少的数据。关注的是"频次",认为用得多的才是真热点。
- 纯当缓存用(数据丢了也能从数据库查) 👉 选 allkeys-lfu。不管有没有设置过期时间,内存满了就按"真实热度"淘汰,最抗造。
- Redis 里既存缓存,又存了不能丢的业务数据 👉 选 volatile-lfu。内存满了只淘汰带过期时间的缓存,那些没设过期时间的业务数据死死保住。
- 里面的数据绝对不能丢(比如当消息队列或计数器用) 👉 选 noeviction。内存满了就直接报错拒绝写入,逼着开发去扩容或者排查问题,绝不偷偷删数据。
- Redis默认是noeviction
6、持久化策略
- AOF(Append Only File),有三种刷磁盘模式
- always 每一条命令都刷磁盘
- everysec(默认) 每秒刷一次盘,最多丢1s数据,性能和安全最佳平衡点,生产标配
- no 交给操作系统决定何时刷盘,但是会丢失大量数据
- RDB(Redis Database)
- 每隔一段时间(或者满足一定写入次数),将内存的全量数据生成一个紧凑的二进制文件(dump.rdb)
- 文件小,恢复快,对主线程影响极小,适合冷备,灾难恢复,跨机房迁移
- 会丢数据,两次快照之间如果宕机,这段时间的写入全没,不适合对数据安全性要求高的场景
- RDB是快照,AOF是日志
- AOF 重写是因为日志文件膨胀,需要精简日志,重写期间有新数据进入,会同时追加到重写缓冲区,AOF缓冲区,保证不丢不重
- RDB+AOF 双开时,重启优先加载 AOF(数据更新)。
- 混合持久化(4.0+):AOF 重写 = RDB头 + AOF尾,兼顾恢复速度与安全,生产推荐开启。
- 重写安全性:通过临时文件 + rename 原子替换,防止重写中途宕机导致文件损坏。