Redis 常用数据结构介绍 – wiki基地


Redis 常用数据结构深度解析

Redis,作为一个高性能的键值存储系统,之所以能够在当今互联网架构中占据举足轻重的地位,不仅仅在于其卓越的读写速度和持久化能力,更在于它提供了丰富且强大的原生数据结构。这些数据结构不仅仅是简单的键值对,它们提供了更复杂的组织数据的方式,使得 Redis 能够胜任缓存、队列、排行榜、计数器等多种应用场景。理解并熟练运用这些数据结构,是发挥 Redis 最大潜能的关键。

本文将深入探讨 Redis 中最常用、也是最核心的五种数据结构:String(字符串)、List(列表)、Set(集合)、Sorted Set(有序集合)和 Hash(哈希)。我们将详细介绍它们的特点、常见用途、核心命令以及底层的实现原理和性能特征。

1. String(字符串)

特点:

Redis 的 String 是最基本的数据类型,它可以存储任何形式的二进制安全数据,包括文本、序列化的对象、图片甚至视频等。它的值可以是字符串、整数或浮点数。String 类型的值最大可以存储 512MB 的数据。

从用途上看,String 类型非常灵活。当存储的是普通字符串时,它可以用于缓存网页内容、JSON 数据、序列化的结构体等。当存储的是数字时,Redis 提供了原子性的递增/递减操作,使其非常适合作为计数器使用。

常见用途:

  1. 缓存: 最常见的用途,直接存储缓存数据(如用户信息、商品详情的 JSON 字符串)。
  2. 计数器: 利用 INCR/DECR 等命令实现网站访问量、点赞数、库存数量等。这些操作是原子性的,在高并发场景下非常安全。
  3. 分布式锁: 可以结合 SETNX (Set if Not Exists) 命令来实现简单的分布式锁。
  4. 存储会话信息: 将用户的 session 信息序列化后存储为 String。
  5. 存储二进制数据: 存储图片、音频等二进制数据片段。

核心命令:

  • SET key value [EX seconds|PX milliseconds|NX|XX]:设置键的值。可以设置过期时间 (EX/PX),或者仅在键不存在时设置 (NX),或仅在键已存在时设置 (XX)。
  • GET key:获取键的值。
  • DEL key [key ...]:删除一个或多个键。
  • INCR key:将键的值递增 1。如果键不存在,会先设置为 0 再执行递增。
  • DECR key:将键的值递减 1。
  • INCRBY key increment:将键的值递增指定的数值。
  • DECRBY key decrement:将键的值递减指定的数值。
  • APPEND key value:将指定值追加到键的末尾。如果键不存在,行为类似于 SET
  • STRLEN key:获取键的值的长度。
  • GETRANGE key start end:获取键的值的子字符串(闭区间)。
  • SETEX key seconds value:设置键的值并指定过期时间(原子操作)。
  • SETNX key value:仅在键不存在时设置值(原子操作)。

底层实现与性能:

Redis 的 String 类型底层实现使用的是 Simple Dynamic String (SDS)。SDS 相比 C 语言传统的字符串(以空字符结尾)有几个优点:

  1. 获取长度快: SDS 结构中直接保存了字符串的长度,获取长度的时间复杂度是 O(1),而 C 字符串需要遍历到空字符,是 O(N)。
  2. 杜绝缓冲区溢出: SDS 在修改字符串时会预留额外的空间,如果修改后的长度超出当前空间,SDS 会自动扩容,避免了 C 字符串常见的缓冲区溢出问题。
  3. 二进制安全: SDS 是通过长度来判断字符串结束的,而不是空字符,因此可以存储包含空字符的二进制数据。

大部分 String 操作,如 GET, SET, DEL, INCR, DECR 的时间复杂度都是 O(1),这是 Redis 快速响应的基础。APPENDGETRANGE 的复杂度取决于操作的长度,但通常也是非常高效的。

2. List(列表)

特点:

Redis 的 List 是一个有序的字符串集合,允许在列表的两端(头部或尾部)进行元素的插入和删除。它的特点是元素按照插入顺序排列,可以有重复的元素。从概念上讲,它类似于一个双向链表,因此在列表的两端进行操作(如 push 和 pop)非常高效。

常见用途:

  1. 消息队列: 利用 LPUSH (或 RPUSH) 作为生产者,RPOP (或 LPOP) 作为消费者,实现简单的消息队列。结合阻塞命令 BLPOP/BRPOP 可以实现阻塞式队列,当队列为空时消费者会阻塞等待新元素。
  2. 最新消息列表: 使用 LPUSH 插入新元素到列表头部,然后使用 LTRIM 保留固定数量的最新元素。
  3. 栈: 使用 LPUSHLPOP 实现栈(后进先出)。
  4. 粉丝列表/关注列表: 如果顺序很重要,可以使用 List 存储。
  5. 任务列表: 存储待处理的任务 ID 或信息。

核心命令:

  • LPUSH key element [element ...]:将一个或多个值插入到列表头部。
  • RPUSH key element [element ...]:将一个或多个值插入到列表尾部。
  • LPOP key:移除并返回列表的头部元素。
  • RPOP key:移除并返回列表的尾部元素。
  • LLEN key:获取列表的长度。
  • LRANGE key start stop:获取列表指定范围内的元素(闭区间,索引可以是负数)。
  • LINDEX key index:获取列表指定索引处的元素。
  • LREM key count value:从列表中移除指定数量的、值为 value 的元素。
  • LTRIM key start stop:修剪列表,只保留指定范围内的元素。常用于限制列表长度。
  • BLPOP key [key ...] timeout:阻塞式地移除并返回第一个非空列表的头部元素。
  • BRPOP key [key ...] timeout:阻塞式地移除并返回第一个非空列表的尾部元素。

底层实现与性能:

Redis 的 List 在不同版本和条件下有不同的底层实现:

  1. Ziplist (压缩列表): 在 Redis 3.0 之前是主要实现之一,适用于存储较少元素且元素本身较小的列表。它将所有元素存储在一块连续的内存中,结构紧凑,节省内存。但插入和删除操作可能需要移动大量数据,效率不高。
  2. Linked List (双向链表): 在 Redis 3.0 之前,当列表元素较多或元素较大时使用,每个节点包含指向前后节点的指针。这使得在两端进行 push/pop 操作非常快(O(1)),但访问中间元素较慢(O(N)),且需要额外的指针开销。
  3. Quicklist (快速列表): 从 Redis 3.2 版本开始成为 List 的默认实现。它是 Ziplist 和 Linked List 的混合体。Quicklist 是一个双向链表,链表的每个节点是一个 Ziplist。这样既保留了双向链表在两端操作的高效率,又利用 Ziplist 存储小元素时的内存效率。

基于 Quicklist 的实现,List 的性能特征如下:

  • LPUSH, RPUSH, LPOP, RPOP: O(1)。在列表两端进行操作非常快。
  • LLEN: O(1)。Quicklist 结构中保存了列表的总长度。
  • LINDEX: O(N)。需要遍历到指定索引,平均复杂度与索引位置有关。
  • LRANGE: O(S+N),其中 S 是起始索引到第一个 Ziplist 节点的距离,N 是要获取的元素数量。获取范围越大,复杂度越高。
  • LREM: O(N)。需要遍历列表寻找并删除元素。
  • LTRIM: O(N)。最坏情况下需要重建列表。

总的来说,List 特别适合需要从两端进行快速存取的场景,如队列和栈。对于需要频繁访问中间元素或进行范围查询且列表很长的场景,需要考虑其 O(N) 性能瓶颈。

3. Set(集合)

特点:

Redis 的 Set 是一个无序的字符串集合,与数学概念中的集合类似。它的最大特点是元素唯一,不允许有重复成员。Set 支持集合间的运算,如交集、并集和差集。

常见用途:

  1. 社交网络应用:
    • 存储用户的标签(tag)。
    • 存储共同关注的人(使用交集)。
    • 存储共同好友(使用交集)。
    • 存储用户关注的人和不感兴趣的人,找出推荐的人(使用差集)。
  2. 去重: 快速判断某个元素是否存在于集合中,或者添加元素时自动去重。例如,统计网站的独立 IP 访问者。
  3. 权限系统: 存储用户拥有的权限列表,判断用户是否具有某个权限。
  4. 商品标签: 存储商品的标签集合,方便进行标签相关的查询和推荐。
  5. 抽奖系统: 使用 SRANDMEMBER 随机抽取集合中的元素。

核心命令:

  • SADD key member [member ...]:向集合中添加一个或多个成员。如果成员已存在,则忽略。
  • SREM key member [member ...]:从集合中移除一个或多个成员。
  • SISMEMBER key member:判断成员是否是集合的成员。
  • SMEMBERS key:返回集合中的所有成员。
  • SCARD key:获取集合的成员数量。
  • SUNION key [key ...]:返回给定所有集合的并集。
  • SINTER key [key ...]:返回给定所有集合的交集。
  • SDIFF key [key ...]:返回给定所有集合的差集。
  • SUNIONSTORE destination key [key ...]:将并集结果存储到指定的键。
  • SINTERSTORE destination key [key ...]:将交集结果存储到指定的键。
  • SDIFFSTORE destination key [key ...]:将差集结果存储到指定的键。
  • SRANDMEMBER key [count]:从集合中随机获取指定数量的成员。
  • SPOP key [count]:随机移除并返回集合中指定数量的成员。

底层实现与性能:

Redis 的 Set 在底层也有两种实现方式:

  1. Intset (整数集合): 如果集合中所有的成员都是整数,并且元素的数量较少,Redis 会使用 Intset 来存储。Intset 将所有整数成员存储在一个有序的、连续的内存块中,非常节省内存。
  2. Hash Table (哈希表): 当集合中的成员不是整数,或者集合大小超过 Intset 的限制时,Redis 会使用哈希表(Dict)。哈希表的键存储集合的成员,值则为 NULL。

基于哈希表实现的 Set 具有以下性能特征:

  • SADD, SREM, SISMEMBER: O(1) (平均)。依赖于哈希表的特性,查找、插入和删除通常非常快。
  • SCARD: O(1)。哈希表结构中保存了元素数量。
  • SMEMBERS: O(N)。需要遍历整个哈希表。
  • 集合运算 (SUNION, SINTER, SDIFF): 复杂度取决于集合的大小,通常是 O(N*M)O(N+M log N),其中 N 和 M 是参与运算集合的大小。如果结果需要存储 (*STORE 命令),还需要额外的写入成本。

Set 适用于需要快速判断成员是否存在、自动去重以及进行集合间运算的场景。由于其无序性,它不适合需要保持元素原有顺序的应用。

4. Sorted Set(有序集合)

特点:

Sorted Set (简称 ZSet) 是 Set 的升级版。它与 Set 类似,成员是唯一的,不允许重复。但与 Set 不同的是,ZSet 中的每个成员都关联着一个分数 (score),这个分数是浮点数。ZSet 中的成员会根据分数从小到大进行排序。如果多个成员有相同的分数,则按字典顺序进行排序。

ZSet 结合了 Set 的唯一性、哈希表的查找效率以及排序的功能,非常强大。

常见用途:

  1. 排行榜: 根据用户的积分、游戏分数、投票数等作为 score,实现各种排行榜(如游戏积分榜、热门文章榜)。
  2. 延迟队列/任务优先级队列: 使用时间戳作为 score,实现按时间顺序执行任务的队列。
  3. 带有权重的标签: 存储带有权重的标签,例如用户对某个主题的兴趣程度。
  4. 基于分数的范围查找: 查找分数在某个范围内的成员(例如,查找积分在 100 到 200 之间的用户)。
  5. 实现简单的推荐系统: 根据用户行为计算权重,然后根据权重排序推荐。
  6. 索引: 可以将 ZSet 作为简单的二级索引,按某个数值属性(如创建时间、更新时间)进行排序和范围查找。

核心命令:

  • ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]:向有序集合中添加一个或多个成员,或更新已有成员的分数。提供了丰富选项控制行为。
  • ZREM key member [member ...]:从有序集合中移除一个或多个成员。
  • ZSCORE key member:获取有序集合中指定成员的分数。
  • ZRANK key member:获取指定成员在有序集合中的排名(从 0 开始,分数由小到大)。
  • ZREVRANK key member:获取指定成员在有序集合中的倒序排名(从 0 开始,分数由大到小)。
  • ZRANGE key start stop [WITHSCORES]:获取有序集合中指定排名范围内的成员。可以带上 WITHSCORES 返回分数。
  • ZREVRANGE key start stop [WITHSCORES]:获取有序集合中指定倒序排名范围内的成员。
  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]:获取有序集合中分数在指定范围内的成员。可以设置开闭区间,带分数,限制返回数量。
  • ZCOUNT key min max:计算有序集合中分数在指定范围内的成员数量。
  • ZCARD key:获取有序集合的成员数量。
  • ZINCRBY key increment member:为有序集合中指定成员的分数加上增量。

底层实现与性能:

Redis 的 Sorted Set 底层主要使用两种数据结构组合实现:

  1. Hash Table (哈希表): 用于存储成员到分数的映射,这样可以通过成员快速查找其分数 (O(1) 平均)。
  2. Skip List (跳跃表): 用于存储成员和分数的有序集合。跳跃表是一种概率型数据结构,它允许快速查找、插入和删除元素,同时保持元素的排序状态。它通过在每个节点维护多个指向后续节点的指针,并随机选择每个节点的层数,来达到平均 O(log N) 的查找、插入和删除效率。

在元素数量较少且元素本身较小时,Redis 也会使用 Ziplist 作为 Sorted Set 的底层实现,以节省内存。

基于哈希表和跳跃表的组合实现,Sorted Set 的性能特征如下:

  • ZADD, ZREM, ZSCORE, ZRANK, ZREVRANK: O(log N) (平均)。插入、删除、查找分数和获取排名都非常高效。
  • ZCARD: O(1)
  • ZRANGE, ZREVRANGE: O(log N + M),其中 N 是有序集合的成员数量,M 是返回的元素数量。查找起始点是 O(log N),遍历范围是 O(M)。
  • ZRANGEBYSCORE, ZCOUNT: O(log N + M) (或 O(log N) 对于 ZCOUNT)。查找分数的起始点是 O(log N),遍历范围是 O(M)。

Sorted Set 是 Redis 中功能最复杂也最强大的数据结构之一,适用于任何需要成员唯一性、按分数排序以及范围查找的场景。

5. Hash(哈希)

特点:

Redis 的 Hash 是一个键值对的集合,其中键是字符串,值也是字符串。它相当于一个嵌套在 Redis 键中的字典或关联数组。每个 Hash 可以存储多对 Field(字段)和 Value(值),Field 和 Value 都是字符串。

Hash 特别适合用来存储对象。一个 Redis Key 对应一个 Hash,这个 Hash 里面的 Field-Value 对可以对应一个对象的属性和属性值。

常见用途:

  1. 存储对象: 将一个对象(如用户信息、商品信息)的各个字段存储在一个 Hash 中,而不是为每个字段创建一个独立的 Redis Key。这减少了 Key 的数量,也使得获取整个对象的所有属性更加高效。
  2. 存储配置信息: 存储某个配置项的所有参数。
  3. 购物车信息: 使用用户 ID 作为 Key,商品 ID 和数量作为 Hash 中的 Field-Value 对。
  4. 缓存用户信息: 将用户 ID 作为 Key,用户的多个属性(姓名、年龄、性别等)作为 Hash 中的 Field-Value 对。

核心命令:

  • HSET key field value [field value ...]:设置 Hash 中一个或多个字段的值。如果字段不存在,则创建。
  • HGET key field:获取 Hash 中指定字段的值。
  • HDEL key field [field ...]:删除 Hash 中一个或多个字段。
  • HKEYS key:获取 Hash 中所有字段的名字。
  • HVALS key:获取 Hash 中所有字段的值。
  • HGETALL key:获取 Hash 中所有字段和值。
  • HLEN key:获取 Hash 中字段的数量。
  • HEXISTS key field:检查 Hash 中指定字段是否存在。
  • HINCRBY key field increment:为 Hash 中指定字段的值加上增量(字段值必须是整数)。
  • HSETNX key field value:仅在字段不存在时设置 Hash 中字段的值。

底层实现与性能:

Redis 的 Hash 在底层也有两种实现方式:

  1. Ziplist (压缩列表): 如果 Hash 中字段数量较少且字段和值本身较小,Redis 会使用 Ziplist 来存储。它将 Field 和 Value 紧凑地存储在一块连续内存中,节省内存。
  2. Hash Table (哈希表): 当 Hash 中字段数量较多或字段/值较大时,Redis 会使用哈希表(Dict)。哈希表的键是 Hash 的 Field,值是 Hash 的 Value。

基于这两种实现,Hash 的性能特征如下:

  • HGET, HSET, HDEL, HEXISTS, HINCRBY: O(1) (平均)。对于单个字段的操作非常高效。
  • HLEN: O(1)
  • HKEYS, HVALS, HGETALL: O(N),其中 N 是 Hash 中字段的数量。需要遍历整个 Hash 表或 Ziplist。

Hash 的优势在于能够将多个相关的键值对组织在一个 Redis Key 下,这比为每个属性创建一个独立的 Key 更节省内存(特别是在使用 Ziplist 编码时)且管理方便。它也非常适合存储结构化的数据对象。然而,HGETALL 等命令在 Hash 非常大时需要注意,因为它会一次性返回所有字段和值,可能导致阻塞。

总结与其他数据结构简述

Redis 的五种核心数据结构——String、List、Set、Sorted Set 和 Hash——各自有着独特的特点和适用场景。它们通过不同的底层实现(SDS、Quicklist/Ziplist、Intset/Hash Table、Skip List/Hash Table)提供了高效的操作性能。

  • String: 简单 KV 存储,计数器,分布式锁。
  • List: 队列、栈、最新列表。
  • Set: 唯一元素集合,去重,社交关系(如共同好友),标签。
  • Sorted Set: 排行榜,带优先级的任务队列,范围查找。
  • Hash: 存储对象,减少 Key 数量。

除了这五种基本数据结构,Redis 还提供了一些更高级或更专业的类型:

  • Bitmap (位图): 实际上是基于 String 类型,通过位操作来实现对字符串的每个比特位进行设置和获取。适用于需要存储大量布尔值(0/1)的场景,如用户每日签到、活跃用户统计等。操作命令有 SETBIT, GETBIT, BITCOUNT, BITOP
  • HyperLogLog: 用于基数估算(统计一个集合中不重复元素的数量)。它可以在极小的内存空间(固定 12KB)内估算一个巨大集合的基数,存在一定的误差(约 0.81%)。适用于需要统计独立访客数等场景。操作命令有 PFADD, PFCOUNT, PFMERGE
  • Geospatial Indexes (地理空间索引): 基于 Sorted Set 实现,用于存储地理位置信息(经纬度),并可以进行半径查询、矩形查询、计算两点距离等。适用于附近的人、地理位置签到等场景。操作命令有 GEOADD, GEODIST, GEORADIUS

掌握这些数据结构是高效使用 Redis 的基石。在设计系统时,根据业务需求选择最合适的数据结构,能够极大地优化性能、节省内存并简化开发逻辑。同时,也需要注意一些潜在的性能问题,比如对大型 List 或 Hash 执行 O(N) 操作可能导致的阻塞,以及大型集合运算的开销。通过合理的设计和对 Redis 命令的理解,可以充分发挥 Redis 的强大能力。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部