深入剖析Redis常用数据结构
Redis 作为互联网架构中无可替代的高性能内存数据库,其读写性能轻松突破10万+ QPS,核心优势不仅在于单线程模型、IO多路复用,更在于其精心设计的数据结构——对外提供简洁易用的操作接口,对内根据数据特征动态选择最优底层实现,在时间复杂度与空间利用率之间做到极致平衡。
本文将从Redis最常用的8种数据结构(String、Hash、List、Set、ZSet、Bitmap、HyperLogLog、GEO)入手,逐层拆解每种结构的核心作用、底层实现原理、编码转换规则,结合源码片段和企业级应用场景,帮你彻底吃透Redis数据存储的底层逻辑,避开使用误区。
一、核心前置认知:Redis的“逻辑类型”与“底层编码”
在深入拆解具体数据结构前,必须先明确一个核心概念:Redis对外提供的“数据结构”(如String、Hash)是逻辑类型,是开发者直接操作的接口;而Redis在内存中真实存储数据的载体,是底层编码结构(如SDS、哈希表、跳表)。
Redis所有的值对象都通过redisObject结构体进行封装,这个结构体是连接逻辑类型与底层编码的核心桥梁,其源码(简化版)如下:
typedef struct redisObject { unsigned type:4; // 逻辑数据类型(String/Hash/List等,占4位) unsigned encoding:4; // 底层编码方式(如int、embstr、hashtable等,占4位) unsigned lru:24; // LRU时间戳或LFU计数,用于内存回收 int refcount; // 引用计数,实现内存自动回收 void *ptr; // 指针,指向底层实际存储数据的结构 } robj;
关键说明:
-
type:标识当前对象的逻辑类型,对应5种基础类型(OBJ_STRING、OBJ_HASH、OBJ_LIST、OBJ_SET、OBJ_ZSET),后续扩展类型(Bitmap等)本质是基于基础类型的封装。
-
encoding:标识底层实际使用的存储结构,Redis会根据数据量、值大小自动切换编码,无需手动干预,核心目的是平衡性能与内存。
-
refcount:引用计数机制,当引用数为0时,Redis自动释放该对象内存,避免内存泄漏。
一句话总结:逻辑类型是“壳”,底层编码是“核”,Redis的高性能本质是“壳简单易用,核极致优化”。
二、Redis常用数据结构详解(按使用频率排序)
1. String(字符串):Redis最基础、最常用的数据结构
1.1 核心作用
String是Redis最基础的数据结构,也是所有其他数据结构的基础,其核心作用是存储字符串、整数、浮点数,支持二进制安全(可存储图片、视频等二进制数据),最大存储容量为512MB。
企业级常用场景:
-
缓存:存储用户信息、商品详情、接口返回结果等(如缓存用户JSON串)。
-
计数器:文章阅读数、商品库存、接口限流计数(依赖INCR/DECR原子操作)。
-
分布式锁:基于SET NX PX命令实现,解决分布式系统并发竞争问题。
-
会话存储:存储用户登录会话信息,设置过期时间实现自动登出。
1.2 底层实现:SDS(Simple Dynamic String)+ 多编码适配
Redis没有使用C语言原生字符串(char*),而是自定义了SDS(简单动态字符串)结构,彻底解决了C字符串的三大缺陷:长度获取O(n)、内存分配频繁、不支持二进制安全。
1.2.1 SDS结构体详解(Redis 7.2+版本)
SDS根据字符串长度不同,提供了5种结构体(sdshdr5已弃用,由sdshdr8替代),核心是“按需分配内存”,减少内存浪费,其核心结构体(sdshdr8)如下:
struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; // 已使用的字节数(O(1)获取长度) uint8_t alloc; // 申请的总字节数(不包括头结构和结尾空字符) unsigned char flags; // 类型标志(3位表示SDS类型,5位未使用) char buf[]; // 柔性数组,存放实际字符串内容(以'\0'结尾,兼容C函数) };
其他SDS类型(sdshdr16/sdshdr32/sdshdr64)仅在len和alloc的类型上有差异,分别对应不同长度范围的字符串:
-
sdshdr8:len范围0~255(1字节),适用于短字符串。
-
sdshdr16:len范围0~65535(2字节),适用于中等长度字符串。
-
sdshdr32:len范围0~2^32-1(4字节),适用于长字符串。
-
sdshdr64:len范围0~2^64-1(8字节),适用于超大字符串。
1.2.2 SDS的核心优势(对比C字符串)
|
对比维度 |
C语言字符串 |
Redis SDS |
|---|---|---|
|
长度获取 |
遍历字符串,时间复杂度O(n) |
内置len字段,时间复杂度O(1) |
|
内存分配 |
修改时需频繁分配/释放内存,易产生碎片 |
预分配+惰性释放,减少内存操作 |
|
二进制安全 |
遇'\0'截断,无法存储二进制数据(如图片) |
以len字段判断结束,支持任意二进制数据 |
|
兼容性 |
原生支持C语言字符串函数 |
buf末尾加'\0',兼容C函数,兼顾性能与兼容性 |
1.2.3 String的编码转换规则
String的底层编码有3种,Redis会根据存储的值自动切换,核心是“小数据省内存,大数据保性能”:
-
OBJ_ENCODING_INT(整数编码):当存储的值是64位符号整数(-2^63 ~ 2^63 -1)时使用。此时redisObject的ptr指针直接存储整数值,无需额外分配SDS空间,内存开销最小。
-
OBJ_ENCODING_EMBSTR(短字符串编码):当存储的字符串长度≤44字节时使用。此时redisObject和SDS结构体在同一块连续内存中分配,减少一次内存分配,降低内存碎片。
-
OBJ_ENCODING_RAW(长字符串编码):当存储的字符串长度>44字节时使用。此时redisObject和SDS分别在两块内存中分配,虽然内存开销略大,但支持更大长度的字符串存储。
注意:编码转换是单向的,只能从int→embstr→raw,无法反向转换。例如,将整数123修改为字符串"1234567890123456789012345678901234567890"(长度40,≤44),编码仍为int;只有修改为非整数或超长字符串,才会切换编码。
2. Hash(哈希):最适合存储对象的数据结构
2.1 核心作用
Hash是一个键值对(field-value)的集合,相当于一个“微型字典”,适合存储具有多个属性的对象(如用户、商品)。与String存储JSON串相比,Hash支持单独操作对象的某个属性(如修改用户年龄),无需获取整个对象,更灵活、更节省内存。
企业级常用场景:
-
对象存储:存储用户信息(id、name、age、city)、商品属性(id、name、price、stock)。
-
购物车:用户购物车(key为用户ID,field为商品ID,value为商品数量)。
-
系统配置:存储系统参数(field为配置项,value为配置值),支持单独修改某一项配置。
2.2 底层实现:listpack(小数据)+ hashtable(大数据)
Hash的底层编码有两种,Redis会根据field数量和field/value长度自动切换,核心是“小数据紧凑存储,大数据高效查询”。其中listpack是Redis后续版本取代ziplist的新一代压缩列表,内存更紧凑、性能更优。
2.2.1 编码切换条件(Redis 6/7默认配置)
-
当Hash的field数量 ≤ hash-max-listpack-entries(默认512),且所有field和value的长度 ≤ hash-max-listpack-value(默认64字节)时,使用
listpack编码。 -
当不满足上述条件时,自动切换为
hashtable编码。
2.2.2 listpack(压缩列表)底层详解
listpack是一种紧凑的连续内存结构,用于存储多个键值对,核心特点是“无指针冗余、内存紧凑”,适合小数据量场景。其结构组成(简化版)如下:
struct listpack { uint32_t total_bytes; // listpack总字节数 uint32_t size; // 存储的元素个数 unsigned char entries[];// 元素数组(field和value交替存储) unsigned char end; // 结束标记(0xFF) };
关键特性:
-
连续内存分配,减少内存碎片,内存利用率极高。
-
元素按“field1、value1、field2、value2”的顺序存储,无需额外指针关联。
-
支持快速定位表尾(通过total_bytes计算),但插入/删除元素时需要移动后续内存,时间复杂度O(n),因此只适合小数据量。
2.2.3 hashtable(哈希表)底层详解
hashtable是Redis的核心底层结构,不仅用于Hash类型,还用于Redis全局键值对的存储(Redis数据库本身就是一个大的hashtable)。其结构与Java的HashMap类似,采用“数组+链表”的结构解决哈希冲突,核心由字典(dict)、哈希表(dictht)、哈希节点(dictEntry)组成。
核心结构体(简化版):
// 哈希表节点(存储单个键值对) typedef struct dictEntry { void *key; // field void *val; // value struct dictEntry *next; // 链表指针,解决哈希冲突 } dictEntry; // 哈希表(数组+链表) typedef struct dictht { dictEntry **table; // 哈希桶数组(存储dictEntry指针) unsigned long size; // 哈希桶数组大小(2的幂) unsigned long sizemask; // 掩码,用于计算哈希值对应的桶索引(size-1) unsigned long used; // 已存储的节点数量 } dictht; // 字典(管理两个哈希表,用于渐进式rehash) typedef struct dict { dictht ht[2]; // 两个哈希表,ht[0]存储数据,ht[1]用于扩容 long rehashidx; // rehash索引(-1表示未进行rehash) } dict;
关键机制:
-
哈希冲突解决:采用链式哈希,当多个field的哈希值对应同一个哈希桶时,用链表将这些节点串联起来。
-
渐进式rehash:当哈希表负载因子(used/size)超过阈值(默认1)时,Redis会触发扩容(将size扩大为原来的2倍)。为避免单线程阻塞,Redis不会一次性拷贝所有数据,而是在每次处理命令时,拷贝一个哈希桶的数据,逐步完成rehash,保证服务不中断。
-
性能:平均时间复杂度O(1)(查找、插入、删除),最坏情况(所有节点都在同一个链表)O(n),但实际应用中极少出现。
3. List(列表):有序可重复的线性结构
3.1 核心作用
List是一个有序、可重复的线性结构,支持从两端插入、删除元素,底层是双向结构,首尾操作效率极高(O(1)),中间操作效率较低(O(n))。适合存储有序的、需要频繁从两端操作的数据。
企业级常用场景:
-
消息队列:基于LPUSH(左进)+ RPOP(右出)实现简单的消息队列,支持阻塞式弹出(BRPOP),避免轮询浪费资源。
-
时间轴:朋友圈动态、用户最新操作记录,基于LPUSH插入新内容,LRANGE获取最新N条数据。
-
栈/队列:LPUSH+LPOP实现栈(先进后出),LPUSH+RPOP实现队列(先进先出)。
3.2 底层实现:quicklist(Redis 3.2+默认)
在Redis 3.2之前,List的底层实现是ziplist(小数据)和linkedlist(大数据),但linkedlist存在内存碎片多、CPU缓存命中率低的问题,ziplist存在插入/删除效率低的问题。因此Redis 3.2+引入了quicklist,将“双向链表+listpack”结合,兼顾空间与效率。
3.2.1 quicklist底层结构详解
quicklist的本质是“双向链表”,但链表的每个节点(quicklistNode)不再存储单个元素,而是指向一个listpack(压缩列表),每个listpack中存储多个元素。结构如下(简化版):
// quicklist节点 typedef struct quicklistNode { struct quicklistNode *prev; // 前驱节点指针 struct quicklistNode *next; // 后继节点指针 unsigned char *zl; // 指向listpack的指针 unsigned int sz; // listpack的字节数 unsigned int count : 16; // listpack中存储的元素个数 unsigned int encoding : 2; // 编码方式(是否压缩) } quicklistNode; // quicklist主结构 typedef struct quicklist { quicklistNode *head; // 头节点 quicklistNode *tail; // 尾节点 unsigned long count; // 所有元素的总个数 unsigned int len; // quicklistNode的个数 } quicklist;
关键特性:
-
兼顾空间与效率:外层双向链表保证首尾操作O(1),内层listpack保证内存紧凑,减少内存碎片。
-
支持压缩:每个listpack可以通过LZF算法压缩,进一步节省内存(默认不压缩,可通过配置开启)。
-
灵活调整:可以通过配置调整每个listpack的最大元素个数(list-max-ziplist-size),平衡内存与性能。
3.2.2 性能分析
-
首尾操作(LPUSH、RPOP、LPOP、RPUSH):O(1),直接操作头节点或尾节点的listpack,效率极高。
-
中间插入/删除(LINSERT、LREM):O(n),需要遍历链表找到目标节点,再在listpack中移动内存,适合小数据量场景。
-
范围查询(LRANGE):O(n),需要遍历链表节点,再从listpack中提取元素,返回的元素越多,效率越低,建议限制返回数量(如LRANGE key 0 9)。
4. Set(集合):无序不可重复的集合结构
4.1 核心作用
Set是一个无序、不可重复的集合,支持交集、并集、差集等集合运算,底层基于哈希表实现,查找效率极高(O(1))。适合存储需要去重、需要进行集合运算的数据。
企业级常用场景:
-
标签去重:用户标签(如“篮球”“音乐”),避免重复添加,支持根据标签筛选用户。
-
共同好友/关注:通过SINTER(交集)获取两个用户的共同好友,通过SDIFF(差集)获取用户的专属关注。
-
抽奖活动:通过SRANDMEMBER(随机获取元素)实现公平抽奖,避免重复中奖。
-
去重统计:存储用户访问记录,自动去重,统计访问人数。
4.2 底层实现:intset(小数据)+ hashtable(大数据)
Set的底层编码有两种,Redis会根据元素类型和数量自动切换,核心是“纯整数小集合省内存,其他场景保性能”。
4.2.1 编码切换条件(Redis默认配置)
-
当Set中的所有元素都是整数,且元素个数 ≤ set-max-intset-entries(默认512)时,使用
intset编码。 -
当不满足上述条件时(如包含字符串元素、元素个数超过512),自动切换为
hashtable编码。
4.2.2 intset(整数集合)底层详解
intset是一种紧凑的整数数组,专门用于存储纯整数集合,核心特点是“有序、无重复、内存紧凑”,支持二分查找。其结构如下(简化版):
typedef struct intset { uint32_t encoding; // 编码方式(INTSET_ENC_INT16/INT32/INT64) uint32_t length; // 元素个数 int8_t contents[]; // 整数数组(存储实际数据,按升序排列) } intset;
关键特性:
-
编码自适应:根据存储的整数大小自动选择编码(INT16:-32768~32767;INT32:-2^31~2^31-1;INT64:-2^63~2^63-1),当插入更大的整数时,会自动升级编码(不可逆)。
-
有序存储:元素按升序排列,支持二分查找,查找效率O(log n)。
-
无重复:插入元素时会先判断是否存在,避免重复存储。
4.2.3 hashtable编码详解
Set的hashtable编码与Hash类型的hashtable底层结构完全一致,区别在于:Set的value值固定为NULL,只需要存储key(即Set中的元素),通过hashtable的key去重特性,实现Set的“不可重复”要求。
性能:插入、删除、查找的平均时间复杂度O(1),集合运算(交集、并集、差集)的时间复杂度取决于两个集合的大小,适合中小规模集合运算。
5. ZSet(有序集合):有序不可重复的加权集合
5.1 核心作用
ZSet(Sorted Set)是一个有序、不可重复的集合,每个元素都关联一个“分数(score)”,Redis通过分数对元素进行排序(升序/降序),支持按分数范围、排名范围查询,是Redis中最强大的数据结构之一。
企业级常用场景:
-
排行榜:商品销量排行、用户积分排行、文章热度排行(按score排序)。
-
延时队列:通过score存储时间戳,按score范围查询实现延时任务(如订单超时取消)。
-
范围统计:统计分数在某个区间的元素个数(如积分在100~200的用户)。
5.2 底层实现:listpack(小数据)+ skiplist+hashtable(大数据)
ZSet的底层编码有两种,Redis会根据元素数量和元素长度自动切换,核心是“小数据紧凑存储,大数据兼顾排序与查询效率”。
5.2.1 编码切换条件(Redis默认配置)
-
当ZSet的元素个数 ≤ zset-max-listpack-entries(默认128),且所有元素的成员长度 ≤ zset-max-listpack-value(默认64字节)时,使用
listpack编码。 -
当不满足上述条件时,自动切换为
skiplist+hashtable混合编码。
5.2.2 listpack编码详解
ZSet的listpack编码与Hash的listpack结构类似,元素按“member、score”的顺序交替存储,且按score升序排列,通过遍历listpack实现排序和查询,适合小数据量场景。
5.2.3 skiplist+hashtable混合编码(核心重点)
当ZSet数据量较大时,Redis采用“跳表(skiplist)+ 哈希表(hashtable)”的混合结构,兼顾“有序排序”和“快速查询”:
-
skiplist:负责维护元素的有序性,支持按score范围、排名范围查询,时间复杂度O(log n)。
-
hashtable:负责通过member快速查找对应的score,时间复杂度O(1),避免遍历跳表查找。
(1)跳表(skiplist)底层详解
跳表是一种有序的数据结构,基于“多层索引”实现高效查询,核心思想是“在链表基础上建立多层索引,快速跳过大量节点,逐层下沉定位目标”,其效率媲美红黑树,但实现更简单。
跳表的结构示意图(简化):
顶层索引:10 → 50 中层索引:10 → 30 → 50 底层链表:10 → 20 → 30 → 40 → 50
跳表的核心结构体(简化版):
// 跳表节点 typedef struct zskiplistNode { sds ele; // 元素member double score; // 分数 struct zskiplistNode *backward; // 前驱节点(用于反向遍历) struct zskiplistLevel { // 每层索引的指针 struct zskiplistNode *forward; // 后继节点 unsigned int span; // 与后继节点的距离(用于计算排名) } level[]; // 动态层数(1~32层) } zskiplistNode; // 跳表主结构 typedef struct zskiplist { struct zskiplistNode *header, *tail; // 头节点、尾节点 unsigned long length; // 元素个数 int level; // 跳表的最大层数 } zskiplist;
关键特性:
-
动态层数:每个节点的层数是随机生成的(1~32层),层数越高,节点越少,查询速度越快。
-
排名计算:通过level[i].span字段记录与后继节点的距离,快速计算元素的排名(如ZRANK命令)。
-
范围查询高效:支持从某个score开始,遍历跳表获取范围内的所有元素(如ZRANGEBYSCORE命令),时间复杂度O(log n + m)(m为返回元素个数)。
(2)混合结构的协同工作
skiplist和hashtable共享同一份元素数据,二者协同工作:
-
插入元素:先在hashtable中插入member→score的映射,再在skiplist中插入该元素,保证两个结构的数据一致。
-
查询元素:通过hashtable快速获取member对应的score(O(1)),通过skiplist快速获取元素的排名或范围(O(log n))。
-
删除元素:同时删除hashtable和skiplist中的元素,保证数据一致性。
6. 扩展数据结构(Bitmap、HyperLogLog、GEO)
以下3种数据结构本质是基于上述基础数据结构的封装,并非独立的底层结构,但在企业级应用中使用频繁,值得重点关注。
6.1 Bitmap(位图):高效的位级操作结构
核心作用:基于String实现,将字符串的每个字节拆分为8个比特位(bit),通过位级操作(SETBIT、GETBIT、BITCOUNT等)存储布尔型数据,极大节省内存(1MB可存储8388608个布尔值)。
企业级场景:用户签到(1表示签到,0表示未签到)、活跃用户统计(BITOR/BITAND实现多日活跃交集)、权限控制(每一位代表一种权限)。
底层实现:本质是String类型,底层还是SDS结构,只是Redis提供了专门的位级操作命令,将SDS的buf数组当作比特数组处理。
6.2 HyperLogLog:基数统计结构
核心作用:用于统计一个集合的“基数”(不重复元素的个数),特点是占用内存固定(约12KB),与集合大小无关,支持海量数据的基数统计(误差率约0.81%)。
企业级场景:UV统计(网站独立访客数)、APP日活/月活统计、海量数据去重统计(无需存储具体元素)。
底层实现:基于String类型,通过“概率算法(HyperLogLog算法)”实现基数统计,核心是通过记录元素的哈希值的前导零个数,估算基数,无需存储具体元素,因此内存占用固定。
6.3 GEO(地理信息):地理位置存储结构
核心作用:基于ZSet实现,用于存储地理位置坐标(经度、纬度),支持根据坐标计算距离、查找附近的元素(如“附近的人”“附近的门店”)。
企业级场景:外卖配送、打车软件、摇一摇、地理位置推荐。
底层实现:本质是ZSet类型,将地理位置坐标(经度、纬度)通过“GeoHash算法”转换为一个64位的整数,作为ZSet的score,将地理位置名称作为member,通过ZSet的排序特性实现地理位置查询。
三、核心总结:Redis数据结构的设计思想
Redis数据结构的底层实现,始终围绕“性能优先、内存优化”的核心思想,总结为3点:
-
分层设计:对外提供简洁的逻辑类型接口,对内根据数据特征动态选择最优底层编码,兼顾易用性与性能。
-
动态适配:同一逻辑类型在不同数据量、数据类型下,自动切换底层编码(如Hash的listpack→hashtable),平衡内存与效率。
-
极致优化:针对不同场景设计专属结构(如SDS解决C字符串缺陷、quicklist兼顾链表与压缩列表优势、skiplist实现高效排序),避免“一刀切”的结构设计。
掌握Redis数据结构的底层实现,不仅能帮助我们更合理地选择数据结构、优化Redis使用性能,还能在面试中脱颖而出——毕竟,Redis数据结构的底层实现,是后端面试的高频考点。
更多推荐


所有评论(0)