跳到主要内容

Redis 数据结构

问题

Redis 有哪些数据结构?底层是怎么实现的?各种数据结构的应用场景是什么?

答案

一、五种基本数据类型

类型说明常用命令典型场景
String字符串/数字/二进制GET, SET, INCR, MGET缓存、计数器、分布式锁
Hash哈希表HGET, HSET, HGETALL对象存储(用户信息)
List双向链表LPUSH, RPOP, LRANGE消息队列、最新列表
Set无序集合SADD, SMEMBERS, SINTER标签、共同好友
ZSet有序集合ZADD, ZRANGE, ZRANGEBYSCORE排行榜、延时队列

二、三种特殊类型

类型说明典型场景
Bitmap位图(基于 String)签到、在线状态
HyperLogLog基数统计UV 统计(误差 ~0.81%)
GEO地理坐标(基于 ZSet)附近的人/店铺
Stream(5.0+)消息流消息队列(类 Kafka)

三、底层编码(重点)

Redis 每种数据类型可能有多种底层编码,会根据数据量 自动转换

类型小数据量编码大数据量编码转换阈值
Stringint / embstrraw (SDS)44 字节
Hashlistpack(7.0+)/ ziplisthashtable128 个 / 64 字节
Listlistpack / ziplistquicklist
Setintset / listpackhashtable128 个 / 64 字节
ZSetlistpack / ziplistskiplist + hashtable128 个 / 64 字节

SDS(Simple Dynamic String)

Redis 不直接用 C 字符串,而是用 SDS:

特性C 字符串SDS
获取长度O(n) 遍历O(1) len 字段
缓冲区溢出不检查,可能溢出自动扩容
二进制安全不安全(遇 \0 截断)安全(用 len 判断结尾)
内存分配每次修改都重新分配预分配 + 惰性释放

跳表(Skip List)— ZSet 的核心

ZSet 在数据量大时使用 skiplist + hashtable 实现:

Level 4:  1 -------------------- 9
Level 3: 1 -------- 5 --------- 9
Level 2: 1 --- 3 -- 5 --- 7 --- 9
Level 1: 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9
操作时间复杂度
查找O(log n)
插入O(log n)
删除O(log n)
范围查询O(log n + k),k 为范围大小
为什么用跳表而不是红黑树?

Redis 作者 antirez 给出的原因:

  1. 跳表实现简单,代码更好维护
  2. 范围查询(ZRANGEBYSCORE)跳表只需找到起点然后遍历链表,红黑树需要中序遍历
  3. 跳表通过调整层数可以在时间和空间之间灵活平衡

listpack(紧凑列表)

Redis 7.0 引入 listpack 替代 ziplist,解决了 ziplist 的连锁更新问题:

  • 将所有元素 紧凑排列 在一块连续内存中
  • 适合元素少的场景(省内存)
  • 元素多了自动转为更高效的数据结构

四、各类型详解与命令

String

SET key value [EX seconds] [NX]  # 设置值(EX 过期时间,NX 不存在才设置)
GET key # 获取值
INCR key # 原子自增
INCRBY key increment # 原子增加指定值
MSET k1 v1 k2 v2 # 批量设置
MGET k1 k2 # 批量获取
SETNX key value # 不存在时设置(分布式锁)

Hash

HSET user:1 name "张三" age 25    # 设置字段
HGET user:1 name # 获取单个字段
HGETALL user:1 # 获取所有字段和值
HMSET user:1 name "张三" age 25 # 批量设置
HINCRBY user:1 age 1 # 字段自增
HDEL user:1 name # 删除字段
HLEN user:1 # 字段数量

ZSet(有序集合)

ZADD ranking 100 "Alice" 95 "Bob" 90 "Carol"  # 添加成员和分数
ZRANGE ranking 0 -1 WITHSCORES # 按分数升序
ZREVRANGE ranking 0 2 # 排名前 3(降序)
ZRANGEBYSCORE ranking 90 100 # 分数范围查询
ZRANK ranking "Alice" # 获取排名
ZINCRBY ranking 5 "Bob" # 分数增加
ZCARD ranking # 成员数量

五、应用场景对照表

场景数据类型实现方式
缓存对象String / HashJSON 字符串或 Hash 字段
计数器StringINCR 原子自增
分布式锁StringSET NX EX
排行榜ZSetZADD + ZREVRANGE
消息队列List / StreamLPUSH + BRPOP / XADD + XREAD
共同好友SetSINTER 交集
签到BitmapSETBIT + BITCOUNT
UV 统计HyperLogLogPFADD + PFCOUNT
附近的人GEOGEOADD + GEORADIUS
限流ZSet / String滑动窗口 / INCR + EXPIRE
延时队列ZSetZADD 时间戳 + ZRANGEBYSCORE

常见面试问题

Q1: String 存储对象和 Hash 存储对象怎么选?

答案

方式做法优点缺点
String + JSONSET user:1 '{"name":"张三","age":25}'序列化简单修改单字段需读写整个 JSON
HashHSET user:1 name "张三" age 25可单独修改字段无法对整个对象设过期时间

选择原则:

  • 读多写少、整体操作 → String
  • 需要频繁修改部分字段 → Hash

Q2: ZSet 底层为什么同时用跳表和哈希表?

答案

  • 跳表:支持范围查询(ZRANGEBYSCORE)和按排名查询(ZRANGE),O(log n)
  • 哈希表:支持按成员查分数(ZSCORE),O(1) 两者结合覆盖了 ZSet 的所有操作需求。

Q3: Redis 的 Stream 和 Kafka 有什么区别?

答案

  • Stream 适合轻量级消息队列,不需要额外部署组件
  • Kafka 是专业的消息系统,吞吐更高、持久化更可靠、支持分区
  • Stream 没有 Kafka 的消费者 offset 提交、分区扩展等高级特性
  • 简单场景用 Stream,高吞吐场景用 Kafka

相关链接