跳到主要内容

数据结构与编码

五大基础类型与底层编码​

关键认知:同一个对外类型,根据数据量和元素大小会切换不同的底层编码。小数据用紧凑结构省内存,超过阈值转成性能更稳的结构。

类型底层编码转换条件
Stringint / embstr / raw纯整数用 int;≤44 字节用 embstr(与对象头连续分配,一次 malloc);更长用 raw(SDS)
Listquicklist(3.2+ 由 ziplist + linkedlist 合并而来,7.0 起内部用 listpack)自适应,无需配置
Hashlistpack / hashtable元素数 ≤ 128 且每个值 ≤ 64 字节用 listpack
Setintset / listpack / hashtable全是整数且 ≤ 512 个用 intset;小字符串集合用 listpack(7.2+);否则 hashtable
ZSetlistpack / skiplist + hashtable元素数 ≤ 128 且每个值 ≤ 64 字节用 listpack;否则跳表 + 字典

对应的配置项是 hash-max-listpack-entries、zset-max-listpack-entries 等。编码转换是单向的 —— 一旦升级为 hashtable/skiplist,即使元素减少也不会退回。

SDS:为什么不用 C 字符串​

Redis 的字符串用自己实现的 SDS(Simple Dynamic String),原因:

  1. O(1) 获取长度 —— 结构体里存了 len,C 字符串要遍历到 \0
  2. 二进制安全 —— 靠 len 判断结尾而不是 \0,所以能存图片等二进制数据
  3. 杜绝缓冲区溢出 —— 拼接前先检查 alloc 空间,不够就自动扩容
  4. 减少内存重分配 —— 空间预分配(小于 1MB 时翻倍预留)+ 惰性释放(缩短时不立刻 free)

跳表:为什么 ZSet 不用红黑树​

ZSet 同时用跳表 + 字典:字典存 member → score 做 O(1) 的分值查询,跳表按 score 有序排列做范围查询。

跳表是多层链表,上层是下层的「索引」,每个节点以 p = 1/4 的概率被提升到上一层,平均查询复杂度 O(log n)。

对比红黑树(面试高频):

  • 范围查询更简单 —— 跳表底层就是有序链表,定位起点后顺序遍历即可;红黑树要做中序遍历,实现复杂
  • 实现与维护简单 —— 跳表插入删除只改相邻指针,不需要旋转和变色
  • 并发友好 —— 局部修改,锁粒度能做得更细
  • 内存可调 —— 通过调整概率 p 平衡内存与性能

各类型的典型用法​

String —— 缓存对象、计数器(INCR)、分布式锁(SET NX)、分布式 ID(INCRBY)。

List —— 消息队列(LPUSH + BRPOP,但有缺陷:不支持多消费者组、消息丢失后无法重放)、最新消息列表。

Hash —— 存对象的各字段,支持单字段更新,比把整个 JSON 存 String 更省流量。但注意 Hash 不能对单个 field 设过期时间(Redis 7.4 才支持 HEXPIRE)。

Set —— 去重、共同好友(SINTER)、随机抽奖(SRANDMEMBER / SPOP)、点赞。注意 SINTER、SUNION 对大集合是 O(n×m),可能阻塞。

ZSet —— 排行榜(ZREVRANGE)、延时队列(score 存执行时间戳)、滑动窗口限流(score 存请求时间戳,ZREMRANGEBYSCORE 清理过期)。

扩展类型​

类型用途关键特性
Bitmap签到、活跃用户统计1 位存一个状态,1 亿用户只需 12MB
HyperLogLogUV 统计固定 12KB 统计上亿基数,标准误差 0.81%,不精确
GEO附近的人底层是 ZSet,用 GeoHash 把二维坐标编码成一维 score
Stream消息队列5.0 新增,支持消费者组、ACK、消息回溯,是 List 做队列的正解

:::info HyperLogLog 的考点 它只能算基数(去重后的数量),不能取出具体元素,且结果有误差。面试问「统计 UV 用什么」答 HLL,追问「要精确值怎么办」答 Set 或 Bitmap(用户 ID 作偏移量)—— 精确的代价是内存线性增长。 :::

单线程模型​

Redis 真的是单线程吗? 要分层回答:

  • 命令执行是单线程 —— 一个主线程串行处理所有命令,所以命令天然原子,不需要加锁
  • 网络 IO 在 6.0 起是多线程 —— io-threads 配置多个线程做 socket 读写和协议解析,缓解网络 IO 瓶颈
  • 后台任务一直有独立线程 —— bio 线程做 UNLINK 的异步释放、AOF fsync、关闭文件描述符

IO 多路复用:单线程靠 epoll 同时监听成千上万个连接,哪个就绪处理哪个,避免为每个连接开线程。这是「单线程还能高并发」的关键。

:::warning 单线程的代价 任何慢命令都会阻塞整个实例。高危命令:KEYS *(遍历全库,用 SCAN 代替)、FLUSHALL、大集合的 SMEMBERS / HGETALL / ZRANGE 0 -1、DEL 一个百万元素的大 key(用 UNLINK 异步删)。 :::