2. Redis 核心数据结构:String、Hash、List、Set、Sorted Set 的使用场景与底层编码

Redis 之所以能成为缓存与读写分离架构中的核心组件,其根本原因在于它提供了丰富且高效的数据结构。理解这五种基础数据结构(String、Hash、List、Set、Sorted Set)的使用场景与底层编码(底层实现),是设计高性能缓存策略、避免数据一致性陷阱的前提。

核心认知: Redis 的每种数据结构都对应着多种底层编码(如 int、embstr、ziplist、skiplist 等)。Redis 会根据存储数据的大小、元素数量等条件,在运行时自动切换编码,以在内存占用与操作性能之间取得最优平衡。

2.1 数据结构全景对比

下表从使用场景、底层编码、读写分离注意事项三个维度,对这五种数据结构进行横向对比,帮助你快速建立全局认知。

数据结构 核心使用场景 底层编码(内部实现) 读写分离注意事项
String 缓存对象、计数器、分布式锁、共享 Session int、embstr、raw 主从延迟可能导致计数器读取不一致;大 Value 需关注序列化开销
Hash 对象属性缓存(如用户信息)、购物车、短数据聚合 ziplist、hashtable 部分字段更新时,从库可能读到旧字段组合;适合做细粒度缓存
List 消息队列、时间轴、最新动态、生产者-消费者模式 quicklist(ziplist + linkedlist 混合) BRPOPLPUSH 等阻塞操作在主从切换时可能丢失消息;需配合队列确认机制
Set 标签系统、好友关系、去重统计、抽奖 intset、hashtable SINTER/SDIFF 等集合运算在主从延迟下结果可能不准确;建议在从库做只读运算
Sorted Set 排行榜、延时队列、优先级任务、范围查询 ziplist、skiplist + hashtable ZRANGEBYSCORE 等范围查询在从库执行时,需确认数据已同步;写操作集中在主库

2.2 String:最基础的缓存单元

String 是 Redis 中最简单的数据结构,但也是使用最频繁的。在读写分离架构中,String 通常承载着热点数据的直接缓存。

2.2.1 使用场景

  • 缓存对象: 将 JSON 序列化后的对象直接存入,如 set user:1001 '{"name":"alice","age":30}'。
  • 计数器: 使用 INCR、DECR 实现点赞数、访问量、库存扣减。
  • 分布式锁: 利用 SETNX 和 SET key value NX PX 30000 实现互斥锁。
  • 共享 Session: 在微服务架构中,将用户 Session 信息以 String 形式集中存储。

2.2.2 底层编码

String 的底层编码根据存储值的不同,分为三种:

  • int: 当存储的字符串可以表示为 64 位有符号整数时,Redis 直接使用整型存储,节省内存且运算极快。
  • embstr: 当字符串长度小于等于 44 字节(Redis 3.2+)时,使用 embstr 编码。它将 RedisObject 和 SDS(Simple Dynamic String)分配在同一块连续内存中,减少内存碎片。
  • raw: 当字符串长度超过 44 字节时,使用 raw 编码。RedisObject 和 SDS 分别分配在不同内存区域。
读写分离提示: 对于大 Value(如超过 10KB 的 String),主从同步时会产生较大的网络带宽消耗,且从库在同步完成前可能返回旧数据。建议对大 Value 进行拆分或使用 Hash 结构。

2.3 Hash:对象级缓存的利器

Hash 结构允许你对一个对象的多个字段进行独立操作,非常适合存储和更新结构化数据,在读写分离场景下能显著减少网络开销。

2.3.1 使用场景

  • 用户信息缓存: HSET user:1001 name "alice" age 30 city "beijing",更新某个字段时无需传输整个对象。
  • 购物车: 以用户 ID 为 key,商品 ID 为 field,商品数量为 value。
  • 短数据聚合: 存储配置项、设备状态等字段数量固定且较少的对象。

2.3.2 底层编码

  • ziplist(压缩列表): 当 Hash 中的字段数量少于 512 个且每个字段的 value 长度小于 64 字节时,使用 ziplist。它将所有键值对按顺序紧凑排列,内存占用极低。
  • hashtable(哈希表): 当不满足 ziplist 条件时,自动升级为 hashtable。此时读写时间复杂度为 O(1),但内存开销更大。
一致性陷阱: 在读写分离架构中,如果主库更新了 Hash 中的某个字段,而从库尚未同步完成,此时读取整个 Hash 对象可能会得到“新旧混合”的数据。建议业务层对关键字段做版本号校验,或优先读取主库。

2.4 List:有序队列的基石

List 是一个有序的字符串列表,支持从两端插入和弹出。在消息队列和流式处理中扮演重要角色。

2.4.1 使用场景

  • 消息队列: 生产者使用 LPUSH,消费者使用 BRPOP 实现阻塞式消费。
  • 时间轴/最新动态: LPUSH 插入最新内容,LRANGE 0 9 获取前 10 条。
  • 生产者-消费者模式: 结合 BRPOPLPUSH 实现可靠队列,确保消息不丢失。

2.4.2 底层编码

  • quicklist(快速列表): 从 Redis 3.2 开始,List 的底层统一使用 quicklist。它是 ziplist 和双向链表的混合体:将数据分段存储在多个 ziplist 中,再用双向链表将这些 ziplist 连接起来。这种设计既保证了内存紧凑,又支持高效的头部和尾部操作。
// 示例:使用 List 实现简单的任务队列
// 生产者
LPUSH task_queue "task:001"
LPUSH task_queue "task:002"

// 消费者(阻塞式获取)
BRPOP task_queue 0
// 返回: 1) "task_queue"  2) "task:001"

2.5 Set:无序集合与去重

Set 是一个无序的、元素唯一的字符串集合。它提供了丰富的集合运算,适合做标签、关系和去重处理。

2.5.1 使用场景

  • 标签系统: 为文章或用户打标签,如 SADD article:1:tags "redis" "cache" "nosql"。
  • 好友关系: 使用 SINTER 计算共同好友,SDIFF 计算可能认识的人。
  • 去重统计: 记录某活动的独立访客 IP,SADD unique_visitors "192.168.1.1"。
  • 抽奖: SRANDMEMBER 随机抽取,SPOP 弹出并移除。

2.5.2 底层编码

  • intset(整数集合): 当 Set 中的所有元素都是整数,且元素数量少于 512 个时,使用 intset。它按升序排列,使用二分查找,内存效率极高。
  • hashtable(哈希表): 当不满足 intset 条件时,升级为 hashtable,保证 O(1) 的增删查操作。

2.6 Sorted Set:带权重的有序集合

Sorted Set 在 Set 的基础上为每个元素关联了一个 double 类型的分数(score),并按照分数排序。它是实现排行榜和延时队列的核心数据结构。

2.6.1 使用场景

  • 排行榜: ZADD leaderboard 100 "player1",ZREVRANGE leaderboard 0 9 WITHSCORES 获取前十名。
  • 延时队列: 将任务执行时间戳作为 score,使用 ZRANGEBYSCORE 获取到期的任务。
  • 优先级任务: 将优先级数值作为 score,高优先级任务自动排在前面。
  • 范围查询: 按分数范围快速定位数据,如查询某个时间段内的订单。

2.6.2 底层编码

  • ziplist(压缩列表): 当 Sorted Set 的元素数量少于 128 个且每个元素的 value 长度小于 64 字节时,使用 ziplist。元素按 score 升序排列。
  • skiplist + hashtable(跳跃表 + 哈希表): 当不满足 ziplist 条件时,使用 skiplist 作为有序结构,同时配合 hashtable 实现 O(1) 的元素查找。跳跃表是一种多层链表结构,支持 O(log N) 的插入、删除和范围查询。
性能关键点: Sorted Set 的 skiplist 编码在写入大量数据时性能稳定,但内存占用较高。在读写分离架构中,建议将排行榜的写操作集中在主库,从库只负责读取,避免从库因同步大量 ZADD 操作而产生延迟。
Redis 五种数据结构底层编码切换机制 Redis 五种数据结构底层编码切换机制 String int (整数) embstr (≤44字节) raw (>44字节) Hash ziplist (小数据) hashtable (大数据) List quicklist (混合) Set intset (整数) hashtable (混合) Sorted Set ziplist (小数据) skiplist+ht (大数据) 编码自动切换条件(Redis 默认阈值) Hash/Sorted Set ziplist:元素 ≤ 512 且 value ≤ 64 字节 Set intset:元素 ≤ 512 且全部为整数 String embstr:长度 ≤ 44 字节(Redis 3.2+) 主库(写操作) 从库(读操作) 异步同步(可能延迟) 建议:写操作(ZADD/HSET/LPUSH)在主库执行;读操作(ZRANGE/HGETALL/LRANGE)在从库执行 注意:从库可能读到旧数据,业务层需容忍最终一致性或强制读主库

2.7 数据结构选择决策树

在实际项目中,面对一个缓存需求,你可以通过以下决策树快速选择最合适的数据结构:

  1. 是否需要持久化单个值? → 是 → String(如缓存 JSON、计数器)
  2. 是否需要操作对象的多个字段? → 是 → Hash(如用户信息、配置项)
  3. 是否需要维护有序列表?
    • 仅需两端操作 → List(如消息队列、时间轴)
    • 需要按分数排序 → Sorted Set(如排行榜、延时队列)
  4. 是否需要去重和集合运算? → 是 → Set(如标签、好友关系)
实战建议: 在读写分离架构中,优先使用 Hash 替代 String 来缓存对象。例如,缓存用户信息时,使用 HSET user:1001 name "alice" age 30 而非 SET user:1001 '{"name":"alice","age":30}'。这样在更新单个字段时,主从同步的数据量更小,从库读取时也能避免反序列化整个对象的开销。