:数据结构)
一、Redis 数据类型全景图Redis 不是简单的 Key-Value 存储它提供了丰富的数据类型每种类型都针对特定场景做了深度优化。理解这些数据类型的底层实现是写出高效 Redis 代码的前提。1.1 五大基础数据类型数据类型底层结构核心特性典型场景StringSDS二进制安全、O(1) 取长度缓存对象、计数器、分布式锁Listquicklist (listpack)双端操作 O(1)、支持阻塞消息队列、时间线、栈/队列Hashlistpack / hashtable字段级读写、节省内存购物车、对象属性缓存Setintset / hashtable去重、集合运算共同关注、抽奖、标签系统ZSetlistpack / skiplistht有序、范围查询排行榜、延时队列、权重排序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 age252. 计数器原子操作# 文章阅读计数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 脚本保证原子性EVALif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end1lock:order:1001 request_id4. 共享 Session在分布式系统中用户的 Session 信息存储在 Redis 中所有服务器实例共享同一份 Session 数据解决了传统 Session 粘滞的问题。2.2 底层实现SDSSimple Dynamic StringRedis 没有直接使用 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:userexample.com}# 消费者阻塞等待超时 30 秒BRPOP queue:tasks302. 时间线/朋友圈点赞# 点赞LPUSH 保证最新在前LPUSH moment:1001:likes user:2001 LPUSH moment:1001:likes user:2002# 取消点赞LREM 移除指定元素LREM moment:1001:likes1user:2001# 获取前 10 个点赞用户LRANGE moment:1001:likes093.2 底层实现演进Redis 的 List 底层经历了三次重大演进版本实现特点问题Redis 3.2双向链表 / ziplist链表灵活ziplist 省内存链表内存不连续ziplist 连锁更新Redis 3.2quicklist双向链表 ziplist 节点平衡了灵活性和内存效率Redis 7.0quicklist (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:20012. 对象缓存的两种方案对比方案命令优点缺点String JSONSET user:1001 {...}简单直观修改单个字段需全量更新HashHSET user:1001 name Alice字段级操作节省网络流量字段多时空 间效率低选型建议对象字段少且整体读取 → String字段频繁变化 → Hash。4.2 底层实现listpack hashtableHash 的编码转换条件当字段数 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:sports2. 抽奖去重# 记录已中奖用户SADD lottery:winners user:1001 user:1002# 判断用户是否已中奖SISMEMBER lottery:winners user:1003# 返回 0未中奖# 随机抽取 1 名未中奖用户从全部用户中排除已中奖SRANDMEMBER all_users15.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:10012. 延时队列# 任务执行时间作为 score时间戳ZADD delay_queue1699123456task:send_emailZADD delay_queue1699123500task:generate_report# 获取当前时间前应执行的任务ZRANGEBYSCORE delay_queue016991234566.2 底层实现listpack skiplist hashtableZSet 是 Redis 中最复杂的数据结构它同时使用了两种数据结构skiplist跳表维护元素的有序性支持范围查询hashtable存储 member → score 的映射保证单点查询 O(1)编码转换条件元素数 128且 member 长度 64 字节→listpack紧凑编码否则 →skiplist hashtable高效编码七、跳表Skip List为什么 Redis 偏爱它7.1 跳表的核心思想跳表是一种基于有序链表的数据结构通过添加多级索引来实现高效的查找、插入和删除操作。它的平均时间复杂度为O(log N)接近平衡树的性能但实现简单得多。跳表查找过程以查找 7 为例从最高层开始层 3 中1 的下一个节点是 99 7向下移动到层 2在层 21 的下一个节点是 55 7向右移动到 5在层 25 的下一个节点是 99 7向下移动到层 1在层 15 的下一个节点是 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-rehashidx0;// 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].used0){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:uv10.3 GEO地理位置服务GEO 基于 ZSet 实现使用GeoHash编码将二维经纬度映射为一维字符串# 添加位置GEOADD locations116.4039.90beijingGEOADD locations121.4731.23shanghai# 获取两地距离GEODIST locations beijing shanghai km# 获取北京附近 100km 的城市GEORADIUS locations116.4039.90100km WITHDIST10.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/BITCOUNTUV/基数统计HyperLogLogPFADD/PFCOUNT位置服务GEOGEOADD/GEORADIUS消息队列StreamXADD/XREADGROUP11.2 编码转换的启示Redis 的每种数据类型都支持多种底层编码并在运行时根据数据特征自动切换小数据 → 紧凑编码listpack、intset节省内存大数据 → 高效编码hashtable、skiplist保证性能这种自适应编码的设计思想是 Redis 在内存效率和执行性能之间取得平衡的关键。