redis的ZSET了解吗?底层是什么结构?

2 个回答

  • 董克平笔记
    垆边人似月,皓腕凝霜雪。
    **ZSET =(跳表 zskiplist 按 score 有序)+(哈希表 dict 做 member→score 快速索引)**。小集合时用**紧凑编码(listpack,旧版本叫 ziplist)**,超过阈值自动转为 **dict+skiplist**。 # 底层结构 * **常规模式(大/中等集合)** ```text zset { dict: member -> score // O(1) 查分数/存在性 zskiplist: 节点按 (score, member) 排序 // O(logN) 范围/排名操作 } ``` * 跳表节点键:先按 **score** 升序,score 相同再按 **member 字典序**。 * `ZADD` 新成员:dict 插入 + 跳表插入;更新分数:先从跳表删除该节点再按新分数插入一次。 * **紧凑模式(小集合)** * **listpack** 顺序存放 `(member, score)` 成对元素,整体按 score 排序;节省内存,但多数操作是 **O(N)**。 * 触发切换的阈值(Redis 7+):`zset-max-listpack-entries`、`zset-max-listpack-value`,超过即转为 **dict+skiplist**。 # 复杂度(核心命令) * `ZADD`:O(logN)(跳表插入) * `ZREM`:O(logN) * `ZSCORE`:O(1)(dict 查) * `ZRANK`/`ZREVRANK`:O(logN)(跳表带 span 计算排名) * `ZRANGE`/`ZREVRANGE`/`ZRANGEBYSCORE`/`ZCOUNT`:O(logN + M)(定位边界 + 输出 M 条) * `ZPOPMIN`/`ZPOPMAX`:O(logN) # 设计要点/常见问题 * **有序依据**:先看 `score` 再看 `member` 字节序,确保稳定且可做 `BYLEX` 范围。 * **分数类型**:`double`,支持 `±inf`;不要用 `NaN`。 * **内存占用**:listpack 很省,但一旦升级为 skiplist+dict,会多一份结构开销(快换空间)。 * **更新代价**:更改分数会做一次“删+插”(仍是 O(logN))。 * **范围查询**:`ZRANGEBYSCORE`/`ZRANGE ... BYSCORE` 更合适按分数分页;同分数下按 member 次序稳定。 一句话记忆:**小集合走 listpack 省内存;一旦变大,ZSET 用“dict 快查 + skiplist 快排”的二合一结构,既能 O(1) 查分数,又能 O(logN) 做范围与排名。**
    发布于 2025-08-19 18:08 •IP属地 福建
    举报
    评论
    收藏

  • 大龙
    慢慢理解世界,慢慢更新自己。
    Redis 快,不是因为“黑魔法”,而是**在内存里做顺序小活 + 单线程事件循环 + 高效数据结构 + 批量/零拷贝 + 异步落盘**;而“命令执行单线程”让它**无需加锁、时延稳定**。自 6.0 起它也引入**I/O 线程**等多线程辅助,但**核心命令仍由单线程执行**。 # 为什么这么快 1. **全内存**:绝大多数操作是 O(1)/O(logN) 的内存读写,避开随机磁盘 I/O。 2. **事件循环 + I/O 多路复用**:单线程 + epoll/kqueue,极少上下文切换/锁竞争。 3. **数据结构为操作而生**:SDS、哈希/跳表、listpack、整数集合、位图、HyperLogLog 等,复杂度低、缓存友好。 4. **批处理/压缩开销**:管道(pipeline)、多键命令把多次系统调用摊薄。 5. **网络与内核路径短**:RESP 协议简单,发送端/接收端复制少,支持 `sendfile`/聚合写。 6. **异步持久化**:AOF fsync、RDB 快照用后台线程/子进程,主线程不被磁盘阻塞。 7. **内存分配优化**:默认 jemalloc,减少碎片/锁竞争,命中局部性更好。 8. **避免慢操作**:过慢命令(O(N) 大 N)默认禁用或有配置阈值,尽量不阻塞主循环。 # 为什么(大多情况下)不把命令执行做成多线程 * **避免锁与复杂一致性**:多线程就要给所有共享结构(哈希、跳表、过期字典、键空间)加细粒度锁或读写锁,复杂且易抖动。 * **时延稳定**:单线程=串行化执行=天然原子,不会因锁竞争/优先级反转拉高尾延迟。 * **真实瓶颈多在网卡/内存带宽**:常见业务 CPU 不是第一瓶颈,多线程收益有限还引入开销。 * **扩展方式更简单**:需要更多并发时,通过**分区/多实例/Cluster 分片**把负载摊到多核/多机,比“一个实例里做细粒度并行”更可控。 # 其实 Redis 也用了多线程/多进程(但不用于命令执行) * **I/O 线程(6.0+ 可选)**:把**读/写套接字**放到工作线程,主线程只做命令解析与执行。 * **BIO 后台线程**:AOF 刷盘、关闭文件、惰性释放大对象等。 * **RDB 快照子进程**:`fork()` + COW,避免阻塞主线程。 * **主动碎片整理、模块线程、复制/集群内部任务**:都可走后台线程。 → 共同点:**耗时 I/O/维护任务**丢后台,**键空间修改仍由主线程串行**。 # 什么时候会慢 & 怎么办 * **单线程 CPU 打满**: * 业务侧:减少大 Key/大范围命令;用批量、流水线;避免 Lua/脚本做重活。 * 服务侧:开 I/O 线程、调大网络缓冲;将实例**横向拆分**(多实例/Cluster 分片),把不同 key 空间分到不同核/机。 * **大 Key/阻塞命令**:拆分大 Key,使用 `UNLINK`(惰性删除),避免 `KEYS/SORT` 等全表扫描。 * **持久化抖动**:AOF 用 `everysec`,RDB 合理安排;磁盘换 SSD,单盘 JBOD。 # 一句话记忆 Redis 的快来自“**内存 + 单线程事件循环 + 针对性数据结构 + 异步 I/O**”。命令执行保持单线程,是为了**无锁、可预测、低尾延**;需要更高并发,用**多实例/Cluster 分片 + I/O 线程**把吞吐线性拉起。
    发布于 2025-08-17 20:39 •IP属地 海南
    举报
    评论
    收藏