Redis 常用数据结构深度解析
Redis,作为一个高性能的键值存储系统,之所以能够在当今互联网架构中占据举足轻重的地位,不仅仅在于其卓越的读写速度和持久化能力,更在于它提供了丰富且强大的原生数据结构。这些数据结构不仅仅是简单的键值对,它们提供了更复杂的组织数据的方式,使得 Redis 能够胜任缓存、队列、排行榜、计数器等多种应用场景。理解并熟练运用这些数据结构,是发挥 Redis 最大潜能的关键。
本文将深入探讨 Redis 中最常用、也是最核心的五种数据结构:String(字符串)、List(列表)、Set(集合)、Sorted Set(有序集合)和 Hash(哈希)。我们将详细介绍它们的特点、常见用途、核心命令以及底层的实现原理和性能特征。
1. String(字符串)
特点:
Redis 的 String 是最基本的数据类型,它可以存储任何形式的二进制安全数据,包括文本、序列化的对象、图片甚至视频等。它的值可以是字符串、整数或浮点数。String 类型的值最大可以存储 512MB 的数据。
从用途上看,String 类型非常灵活。当存储的是普通字符串时,它可以用于缓存网页内容、JSON 数据、序列化的结构体等。当存储的是数字时,Redis 提供了原子性的递增/递减操作,使其非常适合作为计数器使用。
常见用途:
- 缓存: 最常见的用途,直接存储缓存数据(如用户信息、商品详情的 JSON 字符串)。
- 计数器: 利用
INCR
/DECR
等命令实现网站访问量、点赞数、库存数量等。这些操作是原子性的,在高并发场景下非常安全。 - 分布式锁: 可以结合
SETNX
(Set if Not Exists) 命令来实现简单的分布式锁。 - 存储会话信息: 将用户的 session 信息序列化后存储为 String。
- 存储二进制数据: 存储图片、音频等二进制数据片段。
核心命令:
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 语言传统的字符串(以空字符结尾)有几个优点:
- 获取长度快: SDS 结构中直接保存了字符串的长度,获取长度的时间复杂度是 O(1),而 C 字符串需要遍历到空字符,是 O(N)。
- 杜绝缓冲区溢出: SDS 在修改字符串时会预留额外的空间,如果修改后的长度超出当前空间,SDS 会自动扩容,避免了 C 字符串常见的缓冲区溢出问题。
- 二进制安全: SDS 是通过长度来判断字符串结束的,而不是空字符,因此可以存储包含空字符的二进制数据。
大部分 String 操作,如 GET
, SET
, DEL
, INCR
, DECR
的时间复杂度都是 O(1),这是 Redis 快速响应的基础。APPEND
和 GETRANGE
的复杂度取决于操作的长度,但通常也是非常高效的。
2. List(列表)
特点:
Redis 的 List 是一个有序的字符串集合,允许在列表的两端(头部或尾部)进行元素的插入和删除。它的特点是元素按照插入顺序排列,可以有重复的元素。从概念上讲,它类似于一个双向链表,因此在列表的两端进行操作(如 push 和 pop)非常高效。
常见用途:
- 消息队列: 利用
LPUSH
(或RPUSH
) 作为生产者,RPOP
(或LPOP
) 作为消费者,实现简单的消息队列。结合阻塞命令BLPOP
/BRPOP
可以实现阻塞式队列,当队列为空时消费者会阻塞等待新元素。 - 最新消息列表: 使用
LPUSH
插入新元素到列表头部,然后使用LTRIM
保留固定数量的最新元素。 - 栈: 使用
LPUSH
和LPOP
实现栈(后进先出)。 - 粉丝列表/关注列表: 如果顺序很重要,可以使用 List 存储。
- 任务列表: 存储待处理的任务 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 在不同版本和条件下有不同的底层实现:
- Ziplist (压缩列表): 在 Redis 3.0 之前是主要实现之一,适用于存储较少元素且元素本身较小的列表。它将所有元素存储在一块连续的内存中,结构紧凑,节省内存。但插入和删除操作可能需要移动大量数据,效率不高。
- Linked List (双向链表): 在 Redis 3.0 之前,当列表元素较多或元素较大时使用,每个节点包含指向前后节点的指针。这使得在两端进行 push/pop 操作非常快(O(1)),但访问中间元素较慢(O(N)),且需要额外的指针开销。
- 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 支持集合间的运算,如交集、并集和差集。
常见用途:
- 社交网络应用:
- 存储用户的标签(tag)。
- 存储共同关注的人(使用交集)。
- 存储共同好友(使用交集)。
- 存储用户关注的人和不感兴趣的人,找出推荐的人(使用差集)。
- 去重: 快速判断某个元素是否存在于集合中,或者添加元素时自动去重。例如,统计网站的独立 IP 访问者。
- 权限系统: 存储用户拥有的权限列表,判断用户是否具有某个权限。
- 商品标签: 存储商品的标签集合,方便进行标签相关的查询和推荐。
- 抽奖系统: 使用
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 在底层也有两种实现方式:
- Intset (整数集合): 如果集合中所有的成员都是整数,并且元素的数量较少,Redis 会使用 Intset 来存储。Intset 将所有整数成员存储在一个有序的、连续的内存块中,非常节省内存。
- 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 的唯一性、哈希表的查找效率以及排序的功能,非常强大。
常见用途:
- 排行榜: 根据用户的积分、游戏分数、投票数等作为 score,实现各种排行榜(如游戏积分榜、热门文章榜)。
- 延迟队列/任务优先级队列: 使用时间戳作为 score,实现按时间顺序执行任务的队列。
- 带有权重的标签: 存储带有权重的标签,例如用户对某个主题的兴趣程度。
- 基于分数的范围查找: 查找分数在某个范围内的成员(例如,查找积分在 100 到 200 之间的用户)。
- 实现简单的推荐系统: 根据用户行为计算权重,然后根据权重排序推荐。
- 索引: 可以将 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 底层主要使用两种数据结构组合实现:
- Hash Table (哈希表): 用于存储成员到分数的映射,这样可以通过成员快速查找其分数 (O(1) 平均)。
- 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 对可以对应一个对象的属性和属性值。
常见用途:
- 存储对象: 将一个对象(如用户信息、商品信息)的各个字段存储在一个 Hash 中,而不是为每个字段创建一个独立的 Redis Key。这减少了 Key 的数量,也使得获取整个对象的所有属性更加高效。
- 存储配置信息: 存储某个配置项的所有参数。
- 购物车信息: 使用用户 ID 作为 Key,商品 ID 和数量作为 Hash 中的 Field-Value 对。
- 缓存用户信息: 将用户 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 在底层也有两种实现方式:
- Ziplist (压缩列表): 如果 Hash 中字段数量较少且字段和值本身较小,Redis 会使用 Ziplist 来存储。它将 Field 和 Value 紧凑地存储在一块连续内存中,节省内存。
- 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 的强大能力。