【Redis深入】一、快的原因

1. 为什么快

  • 纯内存操作,💡 简单命令微秒级响应(纳秒级是 CPU Cache 访问级别,Redis GET 实际约 50-100μs,避免被挑战)
  • 命令执行单线程,无锁、无上下文切换
  • 自始至终使用 epoll/kqueue 实现 I/O 多路复用,单线程高效处理万级连接
  • Redis 6.0+ 可选开启多线程 I/O,仅并行化网络数据的读写和协议解析,命令执行仍为单线程
  • 💡 紧凑编码体系 :小对象自动采用 intembstrlistpackintset 等紧凑编码,大量小 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
    • 缓存、计数器、分布式锁、Session
  • 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元数据爆炸)
语义层 对象完整性、批量快照、安全删除 ⚠️ 勉强(需应用层额外维护)
计算层 字段级原子运算,卸载客户端逻辑 ❌ 不能(无对应原语)
  • 大对象删除使用 UNLINK 或后台定时删除
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 原子替换,防止重写中途宕机导致文件损坏。
相关推荐
念越1 小时前
【数据库系统概论期末复习】 绪论重点与常考题重点与常考题整理第一章
数据库·数据库系统概论
SXJR1 小时前
langchain4j是如何保证tools或者funcation call不出错的
java·网络·数据库·ai·语言模型
AIMath~1 小时前
兼容pymongo=4.16版本如何安装mongodb
数据库·mongodb
念恒123062 小时前
MySQL连接池原理与简易网站数据流动是如何进行的
数据库·mysql
宇砾2 小时前
浅谈Redis(2)
数据库·redis·缓存
cfm_29142 小时前
Redis Stack 零基础入门
数据库·redis·缓存
海南java第二人2 小时前
ClickHouse 列式存储深度解析:优点、缺点与选型实战
数据库·clickhouse
李白客2 小时前
MySQL迁移操作手册:一次完整迁移的实战路径
数据库·mysql
晴天¥3 小时前
Oracle 19c RAC修改监听默认端口
数据库·oracle