1. 前言
Redis 官方把 Redis 定位为 data structure server,一个内存数据结构服务器。我们使用 Redis 时看到的是 String、List、Hash、Set、Sorted Set、Stream 等逻辑数据类型,而 Redis 内部会根据数据规模、元素大小、元素类型等条件,选择不同的底层编码来存储它们。
也就是说,Redis 的一个数据类型并不一定只对应一种底层结构。比如:
- String 可能是
int、embstr、raw; - List 可能是
listpack或quicklist; - Hash 小的时候可能是
listpack,大了以后会转成hashtable; - Set 小整数集合可能是
intset,普通集合可能是hashtable; - Sorted Set 小的时候可能是
listpack,大了以后通常是skiplist + dict; - Stream 使用的是 radix tree 与 listpack 的组合。
理解这些底层结构,可以帮助我们更好地解释 Redis 为什么快、为什么省内存,以及一些命令在不同数据规模下为什么会有不同表现。
2. Redis 对象模型:type 和 encoding
Redis 中每个 key 对应的 value 都会被包装成一个 Redis 对象。对象中大致包含几类信息:
type:逻辑类型,比如 String、List、Hash、Set、ZSet;encoding:底层编码,比如 int、embstr、raw、listpack、quicklist、hashtable、skiplist;ptr:指向底层数据结构的指针;- 其他元信息:比如引用计数、LRU/LFU 相关信息等。
平时使用 TYPE key 命令看到的是逻辑类型:
TYPE user:1而使用 OBJECT ENCODING key 可以看到该 key 当前的底层编码:
OBJECT ENCODING user:1这两个概念要区分清楚。比如一个 Hash 在业务上仍然是 Hash,但它的底层编码可能是 listpack,也可能是 hashtable。
Redis 会根据数据大小自动选择更节省内存的编码。当数据超过配置阈值,或者某些操作导致紧凑编码不再适合时,Redis 会自动转换成通用编码。这个过程对用户透明,但它会影响内存占用和某些操作的性能。
3. Redis 常见底层数据结构总览
| 逻辑类型 | 常见底层编码 / 数据结构 | 主要特点 |
|---|---|---|
| String | int、embstr、raw,底层依赖 SDS | 二进制安全、支持动态扩容 |
| List | listpack、quicklist | 适合队列、栈、消息列表 |
| Hash | listpack、hashtable | 适合对象字段存储 |
| Set | intset、listpack、hashtable | 无序、去重、集合运算 |
| Sorted Set | listpack、skiplist + dict | 有序、排名、范围查询 |
| Stream | radix tree + listpack | 消息流、消费组 |
| Bitmap | 基于 String | 位级操作,适合签到、状态标记 |
| HyperLogLog | 基于 String 的概率结构 | 估算基数,省内存 |
| Geo | 基于 Sorted Set | 地理位置范围查询 |

下面逐个展开。
4. String:SDS、int、embstr、raw
String 是 Redis 最基础的数据类型。虽然名字叫 String,但它并不只是普通字符串,而是二进制安全的字节序列。图片、JSON、序列化对象、整数、位图数据,都可以放在 String 中。
Redis 没有直接使用 C 语言原生字符串,而是使用 SDS,也就是 Simple Dynamic String。
4.1 为什么不用 C 字符串?
C 字符串以 \0 作为结尾,这会带来几个问题:
第一,获取字符串长度需要遍历,时间复杂度是 O(N)。
第二,字符串中间不能安全地存储 \0,否则会被误认为字符串结束。
第三,拼接字符串时容易出现缓冲区溢出,需要开发者自己处理内存分配。
Redis 作为数据库,需要频繁处理字符串、网络协议、命令参数和 key/value 数据,因此需要一个更安全、更高效的字符串结构。
4.2 SDS 的核心思想
SDS 可以简单理解为:
struct sdshdr { int len; // 已使用长度 int free; // 剩余可用空间 char buf[]; // 实际数据};实际 Redis 中 SDS 会根据字符串长度使用不同 header 类型,以减少内存浪费,但核心思想差不多:在真正的数据 buf 前面记录长度、容量等元信息。
SDS 的优势主要有:
- 获取长度是 O(1)
因为 SDS 直接保存了 len,不需要像 C 字符串那样遍历到 \0。
- 二进制安全
SDS 根据 len 判断字符串长度,而不是依赖 \0,所以中间可以存任意字节。
- 减少频繁内存分配
SDS 扩容时会预留一部分空间,后续追加内容时不一定每次都重新分配内存。
- 兼容部分 C 字符串函数
SDS 的 buf 末尾仍然会保留 \0,所以在一些场景下可以兼容 C 字符串函数。
4.3 String 的三种常见编码
Redis String 常见编码有:
intembstrraw
如果字符串可以表示为整数,并且在 64 位有符号整数范围内,Redis 可能使用 int 编码,节省空间。
如果是比较短的字符串,Redis 可能使用 embstr 编码。embstr 会把 Redis 对象头和 SDS 连续分配在一块内存中,减少一次内存分配,也提升缓存局部性。
如果字符串比较长,Redis 会使用 raw 编码。此时 Redis 对象和 SDS 通常是分开分配的,更适合后续修改和扩容。

可以通过下面命令观察:
SET a 100OBJECT ENCODING a
SET b helloOBJECT ENCODING b
SET c "一个比较长的字符串..."OBJECT ENCODING c5. List:从 linkedlist / ziplist 到 quicklist / listpack
Redis List 是按照插入顺序排列的字符串列表,常用于队列、栈、消息列表等场景。
常见命令包括:
LPUSH queue task1RPUSH queue task2LPOP queueRPOP queueLRANGE queue 0 -1很多人容易把 Redis List 理解成“普通链表”。在早期 Redis 中,List 的确和 linkedlist、ziplist 有关,但现代 Redis 的实现已经发生了变化。
5.1 普通链表的问题
普通双向链表的优点是头尾插入删除很快,时间复杂度是 O(1)。但它也有明显缺点:
- 每个节点都需要额外保存前驱、后继指针;
- 每个元素单独分配内存,内存碎片较多;
- 缓存局部性较差,遍历时可能频繁跳转到不同内存位置。
Redis 是内存数据库,内存占用非常关键。如果 List 中每个元素都用一个独立链表节点,会有不少额外开销。
5.2 listpack 是什么?
listpack 是一种紧凑的连续内存结构,可以在一块连续内存中存储多个元素。
它和数组有点像,都是连续内存,但 listpack 的每个 entry 是变长编码的,可以存整数,也可以存字符串。它主要追求节省内存。
listpack 的优点:
- 元素紧凑存储,指针开销小;
- 缓存局部性较好;
- 适合小集合、小列表、小哈希、小有序集合。

缺点也很明显:
- 中间插入或删除可能需要移动内存;
- 查找某个位置通常需要顺序遍历;
- 数据太大时不适合作为唯一结构。
所以 listpack 适合“小而紧凑”的场景。
5.3 quicklist 是什么?
quicklist 可以理解成“双向链表 + listpack”的组合。
它整体是一个双向链表,每个链表节点内部不是只存一个元素,而是存一个 listpack。也就是说:
quicklist | +-- node1 -> listpack[e1, e2, e3, ...] +-- node2 -> listpack[e4, e5, e6, ...] +-- node3 -> listpack[e7, e8, e9, ...]
这样做相当于在普通链表和连续内存之间做了折中:
- 和普通链表相比,quicklist 减少了节点数量,降低了指针开销;
- 和单个巨大 listpack 相比,quicklist 避免了大范围内存移动;
- 头尾操作仍然比较快;
- 遍历时也能利用 listpack 的连续内存优势。
所以 List 的底层结构可以概括为:小数据尽量紧凑,大数据用 quicklist 分块管理。
5.4 List 的使用建议
List 适合:
- 简单队列:
LPUSH + RPOP; - 简单栈:
LPUSH + LPOP; - 最新消息列表:
LPUSH + LTRIM; - 阻塞队列:
BLPOP、BRPOP。
但 List 不适合频繁按下标随机访问。比如 LINDEX key 100000 这类操作可能需要遍历,性能不如数组型结构。
6. Hash:listpack 与 hashtable
Redis Hash 是 field-value 结构,很适合存储对象。
例如:
HSET user:1 name Tom age 18 city GuangzhouHGET user:1 nameHGETALL user:1业务中可以用 Hash 表示一个用户对象、商品对象、配置对象等。
6.1 小 Hash:listpack
当 Hash 中字段数量较少,并且 field 和 value 都比较短时,Redis 会使用 listpack 编码。
listpack 中会按顺序存储 field 和 value:
[field1, value1, field2, value2, field3, value3]这种方式非常节省内存,因为它不需要为每个 field-value 都维护哈希表节点和指针。
但问题是,查找某个 field 时需要顺序扫描。听起来似乎不快,但由于小 Hash 数据量很小,而且 listpack 连续内存的缓存局部性好,所以实际开销可以接受。
6.2 大 Hash:hashtable
当 Hash 字段数量超过阈值,或者某个 field/value 太大时,Redis 会把它转换成 hashtable 编码。
hashtable 的优点是查询、插入、删除平均时间复杂度接近 O(1),适合字段较多的 Hash。
可以简单理解为:
dict: field1 -> value1 field2 -> value2 field3 -> value3Redis 的哈希表使用链式哈希解决冲突,并支持渐进式 rehash。所谓渐进式 rehash,就是在扩容时不一次性把所有数据迁移完,而是把迁移工作拆散到后续多次操作中逐步完成,避免一次性阻塞太久。

6.3 Hash 的使用建议
Hash 很适合存储对象型数据,比如:
HSET product:1001 name keyboard price 199 stock 50相比于把每个字段拆成多个 key:
SET product:1001:name keyboardSET product:1001:price 199SET product:1001:stock 50使用 Hash 通常能减少 key 的数量,降低 Redis key 元数据开销,也更便于整体管理。
不过需要注意:如果一个 Hash 特别大,HGETALL 可能一次返回大量数据,影响 Redis 单线程执行时间。大 Hash 更推荐使用 HSCAN 分批遍历。
7. Set:intset、listpack、hashtable
Redis Set 是无序、去重的字符串集合,常用于标签、关注关系、去重统计、共同好友等场景。
常见命令:
SADD user:1:tags java redis mysqlSISMEMBER user:1:tags redisSMEMBERS user:1:tagsSINTER set1 set2SUNION set1 set2SDIFF set1 set27.1 intset:整数集合的紧凑表示
如果一个 Set 中的元素全都是整数,并且数量不大,Redis 可能使用 intset 编码。
intset 可以理解为一个有序整数数组。它会根据元素大小选择合适的整数宽度,比如 16 位、32 位、64 位。
当插入更大的整数时,intset 可能会升级编码。例如原本所有元素都能用 16 位表示,后来插入一个需要 32 位表示的整数,那么整个 intset 会升级到 32 位。
intset 的优点是非常省内存,适合小规模整数集合。
缺点是插入、删除、查找通常需要在数组中操作,数据大了以后不如 hashtable。

7.2 listpack:小规模普通集合
在较新的 Redis 版本中,小规模 Set 也可以使用 listpack 编码。它适合元素数量少、元素长度较短的场景。
listpack 的目标依旧是节省内存。
7.3 hashtable:通用 Set 编码
当集合规模变大,或者元素类型不适合紧凑编码时,Redis 会使用 hashtable。
Set 的 hashtable 可以简单理解为:
dict: member1 -> null member2 -> null member3 -> nullvalue 不重要,重要的是 key 是否存在。这样 SISMEMBER、SADD、SREM 等操作平均可以接近 O(1)。
7.4 Set 的使用建议
Set 适合:
- 用户标签;
- 点赞用户集合;
- 抽奖去重;
- 共同关注、共同好友;
- 黑名单、白名单;
- UV 去重。
但要注意集合运算的成本。比如 SINTER、SUNION、SDIFF 在大集合上可能比较重,如果集合非常大,要注意慢查询和阻塞问题。
8. Sorted Set:listpack 与 skiplist + dict
Sorted Set,也叫 ZSet,是 Redis 中非常重要的数据结构。它和 Set 一样要求 member 唯一,但每个 member 都会关联一个 score,Redis 会按照 score 排序。
常见命令:
ZADD rank 100 user1ZADD rank 90 user2ZADD rank 120 user3
ZRANGE rank 0 -1 WITHSCORESZREVRANGE rank 0 -1 WITHSCORESZRANK rank user1ZSCORE rank user1ZRANGEBYSCORE rank 90 120Sorted Set 非常适合:
- 排行榜;
- 延迟队列;
- 滑动窗口限流;
- 按时间戳排序的任务;
- 范围查询。
8.1 小 ZSet:listpack
当 ZSet 元素数量较少,并且 member 较短时,Redis 可能使用 listpack 编码。
listpack 中通常会连续存储 member 和 score:
[member1, score1, member2, score2, member3, score3]这种编码节省内存,但查找和插入需要顺序处理,适合小数据量。
8.2 大 ZSet:skiplist + dict
当 ZSet 规模变大后,Redis 通常会使用 skiplist + dict 的组合。
为什么需要两个结构?
因为 ZSet 既需要根据 member 快速查 score,也需要根据 score 做排序和范围查询。
dict 负责:
member -> score这样 ZSCORE member 可以快速查到分数。
skiplist 负责按照 score 维护有序结构,这样可以支持:
- 按排名查询;
- 按 score 范围查询;
- 获取前 N 名;
- 删除某个 score 范围内的元素。
8.3 skiplist 是什么?
skiplist,跳表,是一种多层有序链表。
最底层包含所有元素,上层是部分元素的索引。查找时从最高层开始,如果当前节点的下一个节点还没超过目标,就继续向前;否则下降一层。通过这种多层索引,可以把查找复杂度降低到平均 O(log N)。
可以简单理解为:
Level 3: 1 -------------------- 9Level 2: 1 -------- 5 --------- 9Level 1: 1 --- 3 --- 5 --- 7 --- 9Level 0: 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9
相比平衡树,跳表实现更简单,范围遍历也比较自然。Redis 的 ZSet 需要大量范围查询,所以 skiplist 很适合这个场景。
8.4 为什么 ZSet 不只用 dict?
如果只用 dict,可以很快通过 member 找到 score,但无法高效按 score 排序和范围查询。
8.5 为什么 ZSet 不只用 skiplist?
如果只用 skiplist,按 score 范围查询很方便,但通过 member 查 score 不够直接。加上 dict 后,可以兼顾 member 查找和 score 排序。
所以 ZSet 的核心思路是:dict 管查找,skiplist 管有序。

9. Stream:radix tree + listpack
Stream 是 Redis 5.0 引入的数据类型,适合消息流场景。
常见命令:
XADD mystream * user Tom action loginXREAD COUNT 10 STREAMS mystream 0XGROUP CREATE mystream group1 0XREADGROUP GROUP group1 consumer1 STREAMS mystream >Stream 中每条消息都有一个 ID,通常形如:
milliseconds-sequence例如:
1710000000000-0Stream 的底层可以理解为 radix tree 和 listpack 的组合。
radix tree,也叫基数树或压缩前缀树,适合存储有序 key。Stream 使用消息 ID 作为索引,可以高效定位某个范围内的消息。
listpack 用来紧凑存储具体消息内容。多条消息可以打包在 listpack 中,从而减少内存开销。
所以 Stream 的整体思路是:
radix tree: message id range -> listpack[messages...]
这种设计适合追加写入、按 ID 范围读取、消息消费组等场景。
10. Bitmap:基于 String 的位操作
Bitmap 不是一种新的独立底层类型,它本质上是把 String 当作 bit 数组来用。
常见命令:
SETBIT sign:2024:03 1001 1GETBIT sign:2024:03 1001BITCOUNT sign:2024:03Bitmap 适合记录大量布尔状态,比如:
- 用户签到;
- 用户是否在线;
- 某天是否活跃;
- 某个功能是否开启;
- 布隆过滤器中的位数组。
Bitmap 的优势是非常省内存。一个用户只需要 1 bit。如果有 1 亿个用户,只记录是否签到,理论上大约只需要 12MB 左右。
缺点是 Bitmap 更适合用户 ID 比较连续的场景。如果 ID 非常稀疏,比如最大用户 ID 很大但真实用户很少,可能会浪费空间。
11. HyperLogLog:基数估算
HyperLogLog 是 Redis 提供的概率型数据结构,常用于估算 UV。
常见命令:
PFADD uv:2024-03-01 user1 user2 user3PFCOUNT uv:2024-03-01PFMERGE uv:2024-03 uv:2024-03-01 uv:2024-03-02它的特点是:
- 内存占用很小;
- 可以估算大量元素的去重数量;
- 结果有一定误差;
- 不能取出具体元素。
所以 HyperLogLog 适合关心“数量”但不关心“具体有哪些元素”的场景。
例如统计网站日活 UV,如果只需要知道大概有多少独立用户访问,用 HyperLogLog 就很合适。如果需要精确判断某个用户是否访问过,那就不适合用它。
12. Geo:基于 Sorted Set 的地理位置索引
Redis Geo 用来存储地理位置信息,例如经纬度。
常见命令:
GEOADD shops 113.2644 23.1291 shop1GEOADD shops 113.2700 23.1300 shop2
GEOSEARCH shops FROMLONLAT 113.2644 23.1291 BYRADIUS 5 kmGeo 的底层和 Sorted Set 有关。Redis 会把经纬度编码成 geohash,然后放到 ZSet 中,用 score 表示位置编码。
因此 Geo 可以利用 ZSet 的有序范围查询能力来查找附近位置。
Geo 适合:
- 附近门店;
- 附近的人;
- 外卖配送范围;
- 地图位置检索。
需要注意的是,Redis Geo 更适合简单地理范围查询。如果要做复杂 GIS 查询,比如多边形包含、复杂路线规划,通常需要专业的地理信息系统。

13. Redis 为什么要设计多种 encoding?
Redis 内部 encoding 的核心目标是:在性能和内存之间做平衡。
对于小数据结构,使用 hashtable、skiplist 这种通用结构虽然操作快,但额外指针和对象头开销比较大。比如一个 Hash 只有几个字段,如果直接用哈希表,元数据可能比真实数据还占空间。
所以 Redis 会在小对象上使用 listpack、intset 这类紧凑结构,牺牲一点查询复杂度,换取更低内存占用。
当数据变大后,顺序扫描、内存移动成本会变高,此时 Redis 再转换成 hashtable、skiplist、quicklist 等更适合大数据量的结构。
可以概括成一句话:
小数据用紧凑编码省内存,大数据用通用结构保性能。
14. 常见误区
14.1 Redis List 就是普通链表吗?
不是。现代 Redis List 主要与 listpack、quicklist 有关。quicklist 是双向链表和 listpack 的组合,每个链表节点中存多个元素。
14.2 Hash 一定是哈希表吗?
不一定。小 Hash 可能是 listpack。只有当数据量或元素大小超过阈值后,才会转成 hashtable。
14.3 ZSet 只用跳表实现吗?
不准确。大 ZSet 通常是 skiplist 和 dict 的组合。skiplist 负责有序范围查询,dict 负责通过 member 快速查 score。
14.4 Bitmap 是单独的数据类型吗?
从使用角度看可以把它当成一种数据结构,但底层本质上是 String 的位操作。
14.5 ziplist 现在还常用吗?
在 Redis 早期版本中,ziplist 用得比较多,比如小 List、小 Hash、小 ZSet 都可能使用 ziplist。但 Redis 7 之后,很多地方已经使用 listpack 替代 ziplist。
15. 如何观察 Redis 的底层编码?
可以使用 OBJECT ENCODING 命令。
示例:
SET k1 100OBJECT ENCODING k1
HSET user:1 name Tom age 18OBJECT ENCODING user:1
ZADD rank 100 user1 90 user2OBJECT ENCODING rank如果想观察编码转换,可以不断插入元素,直到超过阈值:
HSET h f1 v1OBJECT ENCODING h
# 插入大量字段后再次观察HSET h f2 v2 f3 v3 ...OBJECT ENCODING hRedis 的相关阈值可以在配置文件中调整,例如:
hash-max-listpack-entrieshash-max-listpack-valuezset-max-listpack-entrieszset-max-listpack-valueset-max-intset-entriesset-max-listpack-entriesset-max-listpack-value一般不建议随意把阈值调得过大。紧凑编码虽然省内存,但当元素数量变多后,插入、删除、查找可能变慢,还可能在编码转换时带来额外开销。调整前最好结合业务数据规模进行压测。
16. 总结
Redis 快,并不只是因为它把数据放在内存里,还因为它为不同数据类型、不同数据规模设计了不同的底层编码。
String 使用 SDS 解决 C 字符串长度获取、二进制安全和扩容问题。
List 使用 listpack 和 quicklist,在内存占用和头尾操作效率之间做平衡。
Hash 小的时候用 listpack 节省内存,大的时候用 hashtable 保证查询效率。
Set 对小整数集合使用 intset,对小普通集合可以使用 listpack,对大集合使用 hashtable。
Sorted Set 小的时候用 listpack,大的时候用 skiplist + dict,同时支持成员查找和范围排序。
Stream 使用 radix tree + listpack,适合按 ID 追加、读取和消费消息流。
这些设计体现了 Redis 的一个重要思想:不是所有场景都使用同一种通用结构,而是根据数据特点选择最合适的编码。理解这一点之后,我们在使用 Redis 时就能更好地选择数据类型、设计 key、控制 value 大小,并且更合理地分析 Redis 的内存和性能问题。