news 2026/6/12 20:37:06

Redis篇(二):数据结构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis篇(二):数据结构

一、Redis 数据类型全景图

Redis 不是简单的 Key-Value 存储,它提供了丰富的数据类型,每种类型都针对特定场景做了深度优化。理解这些数据类型的底层实现,是写出高效 Redis 代码的前提。

1.1 五大基础数据类型

数据类型底层结构核心特性典型场景
StringSDS二进制安全、O(1) 取长度缓存对象、计数器、分布式锁
Listquicklist (listpack)双端操作 O(1)、支持阻塞消息队列、时间线、栈/队列
Hashlistpack / hashtable字段级读写、节省内存购物车、对象属性缓存
Setintset / hashtable去重、集合运算共同关注、抽奖、标签系统
ZSetlistpack / skiplist+ht有序、范围查询排行榜、延时队列、权重排序

1.2 四大特殊数据类型

数据类型底层实现核心特性典型场景
BitmapString (bit 操作)位级操作、极省内存签到统计、用户在线状态
HyperLogLogString (概率统计)固定 12KB 内存、0.81% 误差UV 统计、基数去重
GEOZSet (GeoHash)经纬度存储、距离计算附近的人、位置服务
StreamRadix Tree + listpack消息 ID、消费者组消息队列、事件流

二、String 类型:不只是字符串

2.1 应用场景深度解析

String 是 Redis 最常用的数据类型,但很多人只把它当作字符串缓存。实际上,String 在 Redis 中是一个"万能容器":

1. 缓存对象

# 直接存储 JSON 字符串SET user:1001'{"id":1001,"name":"Alice","age":25}'GET user:1001# 或使用 Hash 拆分存储(字段频繁变化时推荐)HSET user:1001 name Alice age25

2. 计数器(原子操作)

# 文章阅读计数INCR article:1001:views# 库存扣减(配合 WATCH 实现乐观锁)WATCH stock:1001 GET stock:1001 MULTI DECR stock:1001 EXEC

为什么计数器是原子的?Redis 命令执行是单线程的,INCR/DECR 等操作天然具备原子性,无需额外加锁。

3. 分布式锁(SETNX 的演进)

# Redis 2.6 之前的写法(存在死锁风险)SETNX lock:order:10011EXPIRE lock:order:100130# Redis 2.6+ 推荐写法(原子设置值+过期时间)SET lock:order:1001 request_id NX EX30# 释放锁(Lua 脚本保证原子性)EVAL"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"1lock:order:1001 request_id

4. 共享 Session

在分布式系统中,用户的 Session 信息存储在 Redis 中,所有服务器实例共享同一份 Session 数据,解决了传统 Session 粘滞的问题。

2.2 底层实现:SDS(Simple Dynamic String)

Redis 没有直接使用 C 语言原生的字符串,而是自定义了 SDS 结构。这是 Redis 设计中最精妙的细节之一。

SDS 结构定义(源码简化版)

structsdshdr{uint32_tlen;// 当前字符串长度(O(1) 获取)uint32_talloc;// 已分配的总容量charflags;// SDS 类型标识(5/8/16/32/64 位)charbuf[];// 柔性数组,实际存储数据};

SDS 相比 C 字符串的四大优势

特性SDSC 字符串
获取长度O(1) - 直接读取 len 字段O(N) - 遍历到 \0
缓冲区溢出自动扩容,安全无边界检查,strcat 可能溢出
二进制安全支持,可存储图片、序列化数据等任意二进制不支持,遇 \0 截断
内存预分配支持,减少 realloc 次数不支持,每次精确分配

SDS 扩容策略

  • 当新长度< 1MB时,扩容为2 倍新长度
  • 当新长度≥ 1MB时,扩容为新长度 + 1MB
  • 这种策略在节省内存和减少 realloc 之间取得了平衡

三、List 类型:从双向链表到 quicklist 的演进

3.1 应用场景

1. 消息队列(LPUSH + BRPOP)

# 生产者LPUSH queue:tasks'{"task":"send_email","to":"user@example.com"}'# 消费者(阻塞等待,超时 30 秒)BRPOP queue:tasks30

2. 时间线/朋友圈点赞

# 点赞(LPUSH 保证最新在前)LPUSH moment:1001:likes user:2001 LPUSH moment:1001:likes user:2002# 取消点赞(LREM 移除指定元素)LREM moment:1001:likes1user:2001# 获取前 10 个点赞用户LRANGE moment:1001:likes09

3.2 底层实现演进

Redis 的 List 底层经历了三次重大演进:

版本实现特点问题
Redis < 3.2双向链表 / ziplist链表灵活,ziplist 省内存链表内存不连续,ziplist 连锁更新
Redis 3.2+quicklist双向链表 + ziplist 节点平衡了灵活性和内存效率
Redis 7.0+quicklist (listpack)用 listpack 替代 ziplist彻底解决连锁更新问题

quicklist 的核心设计

structquicklist{quicklistNode*head;// 头节点quicklistNode*tail;// 尾节点unsignedlongcount;// 总元素数intfill;// 每个 ziplist/listpack 节点的最大容量};structquicklistNode{quicklistNode*prev;quicklistNode*next;unsignedchar*zl;// 指向 ziplist/listpack 的指针unsignedintsz;// ziplist 占用的字节数};

quicklist 通过fill参数控制每个压缩列表节点的大小(默认 -2,即 8KB),当单个节点数据量过大时拆分为多个节点,从而规避了连锁更新的风险。


四、Hash 类型:字段级操作的缓存利器

4.1 应用场景

1. 购物车(经典面试题)

# 用户 1001 的购物车HSET cart:1001 sku:20012# 商品 2001,数量 2HSET cart:1001 sku:20021# 商品 2002,数量 1HINCRBY cart:1001 sku:20011# 商品 2001 数量 +1# 获取购物车所有商品HGETALL cart:1001# 获取商品数量HGET cart:1001 sku:2001

2. 对象缓存的两种方案对比

方案命令优点缺点
String + JSONSET user:1001 '{...}'简单直观修改单个字段需全量更新
HashHSET user:1001 name Alice字段级操作,节省网络流量字段多时空 间效率低

选型建议:对象字段少且整体读取 → String;字段频繁变化 → Hash。

4.2 底层实现:listpack + hashtable

Hash 的编码转换条件:

  • 当字段数< 512且所有字段和值的长度< 64 字节→ 使用listpack(紧凑编码,省内存)
  • 否则 → 使用hashtable(标准哈希表,查询 O(1))

五、Set 类型:去重与集合运算的专家

5.1 应用场景

1. 共同关注(交集运算)

# 用户 A 关注的公众号SADD user:A:follows gzh:tech gzh:finance gzh:sports# 用户 B 关注的公众号SADD user:B:follows gzh:tech gzh:finance gzh:food# 共同关注SINTER user:A:follows user:B:follows# 结果: gzh:tech, gzh:finance# A 关注但 B 未关注的SDIFF user:A:follows user:B:follows# 结果: gzh:sports

2. 抽奖去重

# 记录已中奖用户SADD lottery:winners user:1001 user:1002# 判断用户是否已中奖SISMEMBER lottery:winners user:1003# 返回 0,未中奖# 随机抽取 1 名未中奖用户(从全部用户中排除已中奖)SRANDMEMBER all_users1

5.2 底层实现:intset + hashtable

条件编码说明
元素都是整数且数量< 512intset有序整数数组,内存极省
不满足上述条件hashtable标准哈希表,查询 O(1)

intset 的升级机制

intset 中的元素按类型统一存储(int16_t / int32_t / int64_t)。当插入一个更大类型的整数时,整个 intset 会升级为新类型,这是 O(N) 操作,但保证了存储的有序性和内存紧凑性。


六、ZSet 类型:有序集合的王者

6.1 应用场景

1. 实时排行榜

# 添加玩家分数(自动按分数排序)ZADD leaderboard1500player:1001 ZADD leaderboard2300player:1002 ZADD leaderboard1800player:1003# 获取前 10 名(分数从高到低)ZREVRANGE leaderboard09WITHSCORES# 获取玩家排名ZREVRANK leaderboard player:1002# 给玩家加分ZINCRBY leaderboard100player:1001

2. 延时队列

# 任务执行时间作为 score(时间戳)ZADD delay_queue1699123456"task:send_email"ZADD delay_queue1699123500"task:generate_report"# 获取当前时间前应执行的任务ZRANGEBYSCORE delay_queue01699123456

6.2 底层实现:listpack + skiplist + hashtable

ZSet 是 Redis 中最复杂的数据结构,它同时使用了两种数据结构:

  • skiplist(跳表):维护元素的有序性,支持范围查询
  • hashtable:存储 member → score 的映射,保证单点查询 O(1)

编码转换条件

  • 元素数< 128且 member 长度< 64 字节listpack(紧凑编码)
  • 否则 →skiplist + hashtable(高效编码)

七、跳表(Skip List):为什么 Redis 偏爱它?

7.1 跳表的核心思想

跳表是一种基于有序链表的数据结构,通过添加多级索引来实现高效的查找、插入和删除操作。它的平均时间复杂度为O(log N),接近平衡树的性能,但实现简单得多。

跳表查找过程(以查找 7 为例)

  1. 从最高层开始:层 3 中,1 的下一个节点是 9,9 > 7,向下移动到层 2
  2. 在层 2:1 的下一个节点是 5,5 < 7,向右移动到 5
  3. 在层 2:5 的下一个节点是 9,9 > 7,向下移动到层 1
  4. 在层 1:5 的下一个节点是 7,找到目标

时间复杂度分析

操作复杂度说明
查找O(log N)从最高层跳跃查找,逐层缩小范围
插入O(log N)先查找位置,再随机决定节点层数
删除O(log N)查找目标节点,调整前后指针
范围查询O(log N + M)找到起点后,顺序遍历 M 个元素

7.2 为什么 Redis 用跳表而不用平衡树?

这是面试中的高频问题,核心原因有三点:

1. 内存占用更灵活

  • 平衡树每个节点包含 2 个指针(左右子树)
  • 跳表每个节点平均包含1/(1-p)个指针(p 通常取 0.25,即平均 1.33 个)
  • 跳表更节省内存

2. 范围查询更简单

  • 平衡树找到范围起点后,需要中序遍历才能获取后续元素,实现复杂
  • 跳表找到起点后,只需在第 1 层链表顺序遍历即可,实现简单直观

3. 实现难度低

  • 平衡树的插入和删除可能引发子树旋转调整,逻辑复杂,容易出错
  • 跳表的插入和删除只需修改相邻节点的指针,操作简单快速

八、压缩列表的连锁更新与 listpack 的救赎

8.1 ziplist 的连锁更新问题

ziplist 是 Redis 早期为节省内存设计的紧凑数据结构,由连续内存块组成。每个节点包含三个字段:

  • prevlen:前一个节点的长度(1 字节或 5 字节)
  • encoding:当前节点的编码方式
  • content:实际数据

连锁更新的触发条件

假设一个 ziplist 中有多个节点,每个节点的 prevlen 都是 1 字节(表示前一个节点长度 < 254)。当某个节点插入后,其长度变为 254 字节,导致下一个节点的 prevlen 需要从 1 字节扩展为 5 字节。这个扩展又可能导致下一个节点的长度变化,引发连锁反应

最坏情况下,连锁更新需要O(N²)的内存重分配,严重影响性能。

8.2 listpack:彻底解决连锁更新

listpack 是 Redis 5.0 引入、7.0 全面替换 ziplist 的新数据结构。它的核心改进是:

  • 不记录 prevlen,只记录当前节点的长度(len)
  • 插入/修改只影响当前节点,不会触发连锁更新
  • 通过倒序遍历(从尾部开始,根据 len 向前跳)实现反向遍历

Redis 底层数据结构演进时间线

版本改进说明
Redis 3.2quicklist 引入双向链表 + ziplist,平衡灵活性和内存
Redis 5.0listpack 引入解决 ziplist 连锁更新问题
Redis 7.0listpack 全面替换Hash、List、ZSet 等全部使用 listpack

九、哈希表:Redis 的 O(1) 查询基石

9.1 哈希表结构

Redis 的哈希表采用拉链法解决冲突,每个桶(bucket)维护一个链表,哈希值相同的 key 串在一起。

核心特性

  • 负载因子 > 1 时自动扩容:扩容为 2 倍,保证查询效率
  • 渐进式 rehash:分多次迁移数据,避免阻塞主线程(Redis 是单线程的,阻塞会影响所有请求)
  • 查询复杂度:O(1) 平均,最坏 O(N)(所有 key 哈希冲突到同一个桶)

9.2 渐进式 rehash 的精妙设计

Redis 的 rehash 不是一次性完成的,而是分步进行:

// 伪代码:渐进式 rehashdict*d;// 1. 创建新哈希表(2倍大小)d->ht[1]=create_hash_table(d->ht[0].size*2);// 2. 将 rehashidx 置为 0,标志开始 rehashd->rehashidx=0;// 3. 每次执行命令时,迁移少量节点(如 100 个)while(n--&&d->ht[0].used!=0){// 迁移一个桶的所有节点到 ht[1]migrate_bucket(d,d->rehashidx++);}// 4. 全部迁移完成后,交换 ht[0] 和 ht[1]if(d->ht[0].used==0){swap_ht(d);d->rehashidx=-1;// rehash 完成}

这种设计保证了:

  • 查询时:先查 ht[0],再查 ht[1],不会漏数据
  • 写入时:直接写入 ht[1],保证新数据在新表中
  • 无阻塞:每次只迁移少量节点,对性能影响微乎其微

十、特殊数据类型速览

10.1 Bitmap:位级操作的内存杀手

Bitmap 将 String 的每个 bit 作为一个独立的状态位,极省内存:

# 用户签到(第 0 天签到)SETBIT user:1001:sign01SETBIT user:1001:sign11SETBIT user:1001:sign20# 统计本月签到天数BITCOUNT user:1001:sign# 1 亿用户的在线状态仅需 12MB 内存!

10.2 HyperLogLog:概率统计的魔法

HyperLogLog 用固定12KB内存,可以统计2^64个不同元素的基数,标准误差仅0.81%

# 统计网页 UVPFADD page:home:uv user:1001 user:1002 user:1003 PFCOUNT page:home:uv

10.3 GEO:地理位置服务

GEO 基于 ZSet 实现,使用GeoHash编码将二维经纬度映射为一维字符串:

# 添加位置GEOADD locations116.4039.90"beijing"GEOADD locations121.4731.23"shanghai"# 获取两地距离GEODIST locations beijing shanghai km# 获取北京附近 100km 的城市GEORADIUS locations116.4039.90100km WITHDIST

10.4 Stream:消息队列的终极方案

Stream 是 Redis 5.0 专为消息队列设计的数据类型,相比 List 实现的消息队列:

特性ListStream
消息 ID无,需自行维护自动生成全局唯一 ID
消费者组不支持支持,类似 Kafka Consumer Group
持久化无,消费即删除支持,可回溯历史消息
离线重连无法读取历史支持从指定 ID 开始消费
# 生产者XADD mystream * field1 value1 field2 value2# 消费者组XGROUP CREATE mystream mygroup $ XREADGROUP GROUP mygroup consumer1 COUNT1STREAMS mystream>

十一、总结与选型指南

11.1 数据类型选型速查表

需求场景推荐类型关键命令
简单缓存/计数StringSET/GET/INCR
对象字段缓存HashHSET/HGET/HGETALL
队列/栈ListLPUSH/RPOP/LRANGE
去重/集合运算SetSADD/SISMEMBER/SINTER
排行榜/排序ZSetZADD/ZRANGE/ZRANK
签到/状态位BitmapSETBIT/BITCOUNT
UV/基数统计HyperLogLogPFADD/PFCOUNT
位置服务GEOGEOADD/GEORADIUS
消息队列StreamXADD/XREADGROUP

11.2 编码转换的启示

Redis 的每种数据类型都支持多种底层编码,并在运行时根据数据特征自动切换:

  • 小数据 → 紧凑编码(listpack、intset):节省内存
  • 大数据 → 高效编码(hashtable、skiplist):保证性能

这种"自适应编码"的设计思想,是 Redis 在内存效率和执行性能之间取得平衡的关键。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/12 20:34:52

3DMigoto GIMI完整指南:从零开始打造你的专属游戏角色模型

3DMigoto GIMI完整指南&#xff1a;从零开始打造你的专属游戏角色模型 【免费下载链接】GI-Model-Importer Tools and instructions for importing custom models into a certain anime game 项目地址: https://gitcode.com/gh_mirrors/gi/GI-Model-Importer 想要为《原…

作者头像 李华
网站建设 2026/6/12 20:33:55

DS4Windows:免费将PS5手柄完美适配PC游戏的终极指南

DS4Windows&#xff1a;免费将PS5手柄完美适配PC游戏的终极指南 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows 想要在Windows电脑上使用PS5手柄畅玩所有游戏吗&#xff1f;DS4Windows这款…

作者头像 李华
网站建设 2026/6/12 20:31:13

Android 10适配外部存储文件操作的可运行示例工程

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;这个资源包是一套开箱即用的Android 10&#xff08;API 29&#xff09;文件操作代码&#xff0c;专为解决Scoped Storage限制下创建文件夹、写入普通文件和媒体文件的实际问题而设计。项目已配置好targetSdkVer…

作者头像 李华
网站建设 2026/6/12 20:20:41

Go/Rust 系统编程:协程调度与异步运行时的性能对比

Go/Rust 系统编程&#xff1a;协程调度与异步运行时的性能对比一、并发模型之争&#xff1a;Goroutine 与 Tokio 的底层博弈 Go 和 Rust 是当前系统编程领域最受关注的两种语言&#xff0c;它们在并发模型上选择了截然不同的路径。Go 的 Goroutine 采用 M:N 调度模型&#xff0…

作者头像 李华