Redis基础:掌握五种数据类型 – wiki基地


Redis 基础:掌握五种数据类型

引言

在当今高并发、大数据量的互联网应用中,高性能的数据存储和缓存系统扮演着至关重要的角色。Redis 作为一款开源、内存型的数据结构存储系统,凭借其卓越的性能、丰富的功能集以及对多种数据结构的支持,已成为许多企业构建现代化应用的首选。不同于传统的关系型数据库以二维表格的形式组织数据,Redis 基于键值对存储,但其强大之处在于其值(value)不仅可以是简单的字符串,还可以是多种复杂的数据结构。正是这些内置的数据结构,赋予了 Redis 极高的灵活性和多样化的应用场景。

理解并熟练运用 Redis 的这五种基础数据类型——字符串 (String)、列表 (List)、集合 (Set)、有序集合 (Sorted Set/ZSet) 和 哈希 (Hash)——是掌握 Redis 的基石。每种数据类型都有其独特的特性和适用场景,选择正确的数据类型能够极大地优化应用的性能和开发效率。

本文将深入探讨 Redis 的这五种核心数据类型,详细介绍它们的内部实现、常用命令、典型应用场景以及使用时的注意事项,帮助读者构建扎实的 Redis 基础。

1. 字符串 (String)

1.1 描述

String 是 Redis 中最基础的数据类型,也是最简单的一种。它可以存储任何形式的字节序列,包括文本、二进制数据、序列化的对象(如 JSON、Protobuf)等。一个 Redis String 可以存储最大 512MB 的数据。

尽管被称为“字符串”,但 Redis 的 String 是二进制安全的 (binary-safe)。这意味着你可以存储任何字节序列而无需担心其内容或编码。String 类型也可以用来存储整数或浮点数,Redis 提供了对存储的数字进行原子性增减的命令,使其非常适合用作计数器。

1.2 内部实现

为了效率和内存优化,Redis 的 String 并不是简单地使用 C 语言的字符串(以 \0 结尾)。Redis 使用了一种名为 Simple Dynamic String (SDS) 的结构。SDS 相较于 C 字符串有以下优势:

  • 记录长度: SDS 结构中包含记录字符串长度的字段,获取字符串长度的复杂度是 O(1),而 C 字符串需要遍历 O(N)。
  • 杜绝缓冲区溢出: SDS 在修改(如 APPEND)时,会根据需要进行空间预分配,避免了缓冲区溢出问题。
  • 二进制安全: SDS 不依赖于特定的结束符,可以安全存储包含 \0 字节的二进制数据。

此外,Redis 根据 String 的实际内容和长度,会使用不同的内部编码方式:

  • int: 如果 String 存储的是一个可以用 long long 类型表示的整数,会直接存储为整数,非常节省内存。
  • embstr: 如果 String 长度小于等于 44 字节(在 Redis 3.2 之前是 39 字节),会使用 embstr 编码。这种编码方式会将 SDS 结构和实际的字符串数据分配在同一块连续的内存区域,减少内存分配次数和碎片。
  • raw: 如果 String 长度大于 44 字节,则使用 raw 编码,SDS 结构和实际数据分别存储在不同的内存区域。

1.3 常用命令

  • SET key value [EX seconds] [PX milliseconds] [NX|XX]:设置键的值。EXPX 设置过期时间,NX 只在键不存在时设置,XX 只在键存在时设置。
  • GET key:获取键的值。
  • DEL key [key ...]:删除一个或多个键。
  • MSET key1 value1 key2 value2 ...:同时设置多个键的值。
  • MGET key1 key2 ...:同时获取多个键的值。
  • INCR key:将键存储的数字值原子性地增加 1。
  • DECR key:将键存储的数字值原子性地减少 1。
  • INCRBY key increment:将键存储的数字值原子性地增加指定的增量。
  • DECRBY key decrement:将键存储的数字值原子性地减少指定的减量。
  • APPEND key value:如果键已经存在,则将 value 追加到键当前值的末尾;如果键不存在,则设置键的值为 value。
  • STRLEN key:获取键存储的字符串的长度。
  • GETRANGE key start end:获取字符串的子字符串。
  • SETRANGE key offset value:用指定的 value 覆盖字符串从 offset 开始的部分。

1.4 典型应用场景

  • 缓存: 最常见的用途。将数据库查询结果、计算结果等存储为字符串,快速响应客户端请求。例如,SET user:1:profile '{...}'
  • 计数器: 使用 INCR/DECR/INCRBY/DECRBY 实现原子性的计数,如网站访问量、点赞数、商品库存。例如,INCR page:view:article:100
  • 分布式锁: 利用 SET key value NX EX seconds 命令的原子性来实现简单的分布式锁。NX 保证只有不存在时才设置成功,EX 设置过期时间防止死锁。
  • Session 存储: 将用户的 Session 信息序列化后存储为 String。例如,SET session:user:abcde '{...}' EX 3600
  • 存储序列化对象: 存储 JSON、XML、Protobuf 等序列化后的数据。

1.5 注意事项

  • 虽然可以存储大字符串,但过大的字符串会占用较多内存,且在网络传输时也可能成为瓶颈。
  • 原子性计数操作非常高效,但在高并发场景下需注意其他操作(如 GET、SET)与计数操作的竞争。

2. 列表 (List)

2.1 描述

List 是一个有序 (ordered) 的字符串元素集合。它可以从列表的头部 (left)尾部 (right) 添加(push)或移除(pop)元素。列表中的元素可以通过索引进行访问,但获取中间元素的效率不如从两端操作。

列表的特点使其非常适合实现队列 (queue) 和栈 (stack) 这两种常见的数据结构。

2.2 内部实现

在 Redis 3.2 版本之前,List 的底层实现是根据元素数量和大小混合使用 ziplist (压缩列表)linkedlist (双向链表)。ziplist 节省内存但不利于修改,linkedlist 方便修改但占用内存较多。

从 Redis 3.2 开始,List 的底层实现统一改用 Quicklist。Quicklist 是一个双向链表,但链表中的每个节点不再是一个简单的元素,而是一个 ziplist。这种结构结合了 linkedlist 的方便修改(尤其是两端操作)和 ziplist 的内存效率,是当前 Redis List 的主要实现方式。

  • ziplist: 压缩列表是一种紧凑的内存结构,用于存储小整数或短字符串。它将多个元素连续存储在内存中,减少了内存碎片和指针开销。适用于存储少量、长度较小的元素的列表。
  • linkedlist: 双向链表,每个节点包含指向前一个和后一个节点的指针。适用于存储大量元素或元素长度较大的列表,因为在链表头部或尾部增删元素的效率很高(O(1))。

Quicklist 通过控制每个 ziplist 节点能容纳的元素数量以及是否压缩 ziplist 节点,可以在内存使用和访问速度之间进行权衡。

2.3 常用命令

  • LPUSH key element [element ...]:将一个或多个值插入到列表头部。
  • RPUSH key element [element ...]:将一个或多个值插入到列表尾部。
  • LPOP key:移除并获取列表的第一个元素。
  • RPOP key:移除并获取列表的最后一个元素。
  • LRANGE key start stop:获取列表指定范围内的元素。索引从 0 开始,-1 表示最后一个元素。
  • LINDEX key index:获取列表中指定索引的元素。
  • LLEN key:获取列表的长度。
  • LTRIM key start stop:保留列表中指定范围内的元素,移除范围外的元素。常用于限制列表长度。
  • LREM key count value:从列表中移除与 value 相等的元素。count > 0 从头部开始移除 count 个,count < 0 从尾部开始移除 |count| 个,count = 0 移除所有相等的元素。
  • LINSERT key BEFORE|AFTER pivot value:在列表中 pivot 元素的前面或后面插入 value。
  • BLPOP key1 [key2 ...] timeout:从第一个非空列表的头部移除并获取一个元素,如果所有列表都为空,则阻塞直到有元素可用或超时。阻塞版本的 LPOP。
  • BRPOP key1 [key2 ...] timeout:阻塞版本的 RPOP。

2.4 典型应用场景

  • 消息队列: 使用 LPUSH 生产者将消息推入列表,消费者使用 RPOPBLPOP 从列表尾部取出消息(或反过来使用 RPUSH/LPOP)。BLPOPBRPOP 实现阻塞等待,非常适合构建简单的消息队列。
  • 栈: 使用 LPUSH 入栈,LPOP 出栈。
  • 时间线/动态流 (Timeline/Feed): 将用户最新发布的动态按时间顺序存储到列表中。使用 LPUSH 添加新动态,LRANGE 获取最新动态列表。使用 LTRIM 限制只保留最新的 N 条动态。
  • 历史记录: 存储用户的浏览历史、操作历史等。例如,LPUSH user:100:history "item:abc",并用 LTRIM 限制长度。

2.5 注意事项

  • LRANGE 命令虽然可以获取指定范围的元素,但对于非常大的列表,获取整个列表的效率较低,因为它需要遍历。通常用于分页获取数据。
  • LINDEX 的性能对于 Quicklist 结构来说,理论上会受制于需要遍历链表节点找到目标 ziplist,然后在 ziplist 中查找元素。但在实践中,Redis 的 Quicklist 做了优化,性能相对不错,但对于极长列表的随机索引访问仍不如 Hashtable 快。
  • 避免在生产环境中对巨大的 List 执行 LINDEXLRANGE 获取大量元素的慢操作,可能阻塞 Redis。

3. 集合 (Set)

3.1 描述

Set 是一个无序 (unordered) 的字符串元素的集合。它最重要的特性是集合中的元素是唯一的 (unique),不允许有重复的成员。

Set 提供了非常高效的成员添加、删除、查找操作,其时间复杂度通常为 O(1)。此外,Redis 的 Set 支持丰富的数学集合运算,如并集、交集、差集等。

3.2 内部实现

Set 的内部实现也是根据元素数量和特性进行优化的:

  • intset (整数集合): 如果集合中所有的元素都是整数值,并且元素的数量较少(小于某个阈值,默认为 512)且整数值的范围在一个适当的范围内,Redis 会使用 intset 编码。intset 是一种紧凑的数组结构,元素有序存放,查找效率较高且非常节省内存。
  • hashtable (哈希表): 当集合中的元素不是整数,或者元素数量超出 intset 的阈值时,Redis 会使用哈希表来实现 Set。哈希表的键是集合的成员,值是 NULL。查找、添加、删除操作的平均时间复杂度是 O(1)。

3.3 常用命令

  • SADD key member [member ...]:向集合添加一个或多个成员。
  • SMEMBERS key:获取集合中的所有成员。注意,因为 Set 是无序的,所以返回的顺序是随机的。
  • SISMEMBER key member:判断 member 元素是否是集合 key 的成员。
  • SCARD key:获取集合的成员数量。
  • SPOP key [count]:随机移除并返回集合中的一个或多个元素。
  • SRANDMEMBER key [count]:随机返回集合中的一个或多个元素,但不移除
  • SREM key member [member ...]:移除集合中的一个或多个成员。
  • SUNION key1 [key2 ...]:计算一个或多个集合的并集。
  • SINTER key1 [key2 ...]:计算一个或多个集合的交集。
  • SDIFF key1 [key2 ...]:计算一个或多个集合的差集 (key1 减去 key2, key3 …)。
  • SUNIONSTORE destination key1 [key2 ...]:计算并集并将结果存储到 destination 集合。
  • SINTERSTORE destination key1 [key2 ...]:计算交集并将结果存储到 destination 集合。
  • SDIFFSTORE destination key1 [key2 ...]:计算差集并将结果存储到 destination 集合。

3.4 典型应用场景

  • 标签系统: 存储一个对象(如一篇文章、一个商品)的所有标签。例如,使用一个 Set 存储文章 ID 为 100 的所有标签:SADD article:100:tags "科技" "互联网" "AI"。要获取所有标签,使用 SMEMBERS article:100:tags
  • 社交关系: 存储用户的关注/粉丝列表。例如,用户 100 关注的用户列表:SADD user:100:following 201 305 410。判断用户 100 是否关注了用户 201:SISMEMBER user:100:following 201
  • 共同好友/兴趣: 使用 SINTER 计算两个用户的共同好友或共同标签。例如,SINTER user:100:following user:200:following
  • 统计网站独立访问者 (UV): 每天用一个 Set 存储访问用户的 ID,利用 Set 的唯一性,SCARD 命令即可获取当天的 UV。例如,SADD daily:uv:20231026 user:abc user:def user:abc (user:abc 只会添加一次)。
  • 商品推荐: 根据用户的标签或购买历史,通过集合运算找到有相似兴趣的用户或商品。

3.5 注意事项

  • SMEMBERS 会获取集合的所有成员,对于非常大的集合,这可能是一个耗时且占用网络带宽的操作。如果只需要判断某个元素是否存在,使用 SISMEMBER 更高效。
  • 集合运算 (SUNION, SINTER, SDIFF) 在处理大量大集合时可能会消耗较多 CPU 资源。如果需要频繁进行集合运算并复用结果,考虑使用 *STORE 命令将结果存储到新的 Set 中。

4. 有序集合 (Sorted Set / ZSet)

4.1 描述

Sorted Set(简称 ZSet)与 Set 类似,也是字符串元素的集合,且元素是唯一的,不允许有重复的成员。然而,与 Set 不同的是,ZSet 的每个成员都关联着一个分数 (score),这个分数是一个浮点数。ZSet 中的成员总是根据其分数进行排序的,分数可以重复,但成员必须唯一。

通过分数值,可以轻松地按分数范围或排名范围获取 ZSet 中的成员。这使得 ZSet 成为排行榜、带有优先级的队列等场景的理想选择。

4.2 内部实现

Sorted Set 的底层实现主要依赖于两种数据结构:

  • ziplist (压缩列表): 当 Sorted Set 包含的成员数量较少(默认小于 128 个)且所有成员的长度都较短(默认小于 64 字节)时,Redis 使用 ziplist 编码。在这种情况下,成员和分数被紧凑地存储在 ziplist 中,非常节省内存。
  • skiplist (跳跃表) 和 hashtable (哈希表): 当不满足 ziplist 的条件时,Sorted Set 使用跳跃表和哈希表两种数据结构协同工作。
    • hashtable: 用于存储成员到分数的映射(member -> score),这样可以通过成员快速查找其分数,复杂度为 O(1)。
    • skiplist: 用于存储成员到分数的映射(score -> member),并按照分数大小进行排序。跳跃表是一种概率型数据结构,插入、删除、查找以及范围查询的平均时间复杂度都是 O(log N),在实践中表现非常优秀。跳跃表使得 Redis 可以在 O(log N) 的时间复杂度内快速按分数范围或排名范围查询成员。

4.3 常用命令

  • ZADD key score member [score member ...]:向有序集合添加一个或多个成员,或者更新已存在成员的分数。
  • ZRANGE key start stop [WITHSCORES]:按照分数从小到大(升序)获取指定排名范围的成员。WITHSCORES 可以同时返回分数。
  • ZREVRANGE key start stop [WITHSCORES]:按照分数从大到小(降序)获取指定排名范围的成员。
  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]:按照分数范围获取成员(升序)。LIMIT 可以用于分页。
  • ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]:按照分数范围获取成员(降序)。注意 max 和 min 的顺序。
  • ZREM key member [member ...]:移除有序集合中的一个或多个成员。
  • ZCARD key:获取有序集合的成员数量。
  • ZSCORE key member:获取指定成员的分数。
  • ZRANK key member:获取指定成员的排名(从 0 开始,分数越低排名越靠前)。
  • ZREVRANK key member:获取指定成员的排名(从 0 开始,分数越高排名越靠前)。
  • ZCOUNT key min max:获取分数在指定范围内的成员数量。
  • ZINCRBY key increment member:原子性地增加指定成员的分数。
  • ZSCAN key cursor [MATCH pattern] [COUNT count]:迭代有序集合中的元素(适用于大集合,避免阻塞)。

4.4 典型应用场景

  • 排行榜: 最经典的场景。使用 ZSet 存储玩家分数和对应的玩家 ID。ZADD 更新分数,ZREVRANGE 获取 Top N 玩家,ZRANK/ZREVRANK 获取玩家排名,ZRANGEBYSCORE 获取分数在某个范围内的玩家。
  • 带有权重的任务队列: 使用分数字段作为任务的优先级或执行时间戳。
  • 延迟队列: 将任务的执行时间戳作为 score,到期后使用 ZRANGEBYSCORE 结合 ZREM 取出到期的任务执行。
  • 范围查找: 存储带有时间戳或数值度量的数据,通过 ZRANGEBYSCORE 进行范围查找。例如,存储商品的销售额和 ID,查找销售额在某个范围内的商品。
  • 实时榜单: 根据用户的活跃度、贡献值等作为分数,实时更新榜单。

4.5 注意事项

  • Sorted Set 的范围查询和排名查询非常高效 (O(log N) 或 O(log N + M),M 是返回的元素数量),是其核心优势。
  • ZADD 命令在添加新成员或更新成员分数时,时间复杂度是 O(log N)。
  • 虽然分数可以重复,但成员必须唯一。
  • 对于非常大的 Sorted Set,ZRANGEZREVRANGE 如果返回大量元素,可能会占用较多网络带宽和客户端内存。可以使用 LIMIT 参数进行分页,或考虑使用 ZSCAN 进行迭代。

5. 哈希 (Hash)

5.1 描述

Hash (哈希) 是一个存储键值对的无序散列表。它将多个字段 (field) 和值 (value) 关联到一个 Redis 键上。从概念上讲,它类似于关系型数据库中的一行记录,或者编程语言中的一个对象或字典。

Hash 非常适合用于存储对象,例如用户的详细信息、商品的属性等。每个 Hash 可以包含高达 2^32 – 1 个字段-值对。

5.2 内部实现

Hash 的底层实现也根据字段数量和值的大小进行了优化:

  • ziplist (压缩列表): 当 Hash 中包含的字段数量较少(默认小于 512 个)且所有字段名和值都较短(默认小于 64 字节)时,Redis 使用 ziplist 编码。ziplist 以紧凑的方式存储字段和值对,非常节省内存。
  • hashtable (哈希表): 当不满足 ziplist 的条件时,Redis 使用哈希表来实现 Hash。外部哈希表的键是 Hash 的字段名,值是字段对应的值。这种方式提供了 O(1) 的平均时间复杂度来进行字段的查找、添加、修改和删除。

5.3 常用命令

  • HSET key field value [field value ...]:设置哈希表中一个或多个字段的值。如果字段不存在,则创建;如果已存在,则覆盖。
  • HGET key field:获取哈希表中指定字段的值。
  • HMSET key field1 value1 field2 value2 ...:同时设置多个字段的值 (在 Redis 4.0 后,HSET 已经可以实现 HMSET 的功能,HMSET 在新版本中可能被标记为废弃或建议使用 HSET)。
  • HMGET key field1 [field2 ...]:同时获取哈希表中一个或多个字段的值。
  • HGETALL key:获取哈希表中所有字段和值。返回一个列表,包含 field1, value1, field2, value2 … 的顺序。
  • HDEL key field [field ...]:删除哈希表中一个或多个字段。
  • HEXISTS key field:判断哈希表中指定字段是否存在。
  • HKEYS key:获取哈希表中所有字段名。
  • HVALS key:获取哈希表中所有值。
  • HLEN key:获取哈希表中字段的数量。
  • HINCRBY key field increment:原子性地增加哈希表中指定字段的数字值。
  • HSCAN key cursor [MATCH pattern] [COUNT count]:迭代哈希表中的字段和值(适用于大哈希表)。

5.4 典型应用场景

  • 存储对象: 存储用户的属性信息,如用户 ID、用户名、年龄、性别等。例如,HSET user:100 id 100 username "alice" age 30。获取用户所有信息:HGETALL user:100。获取特定信息:HGET user:100 username
  • 存储商品信息: 存储商品的 SKU、名称、价格、库存等属性。
  • 配置信息: 存储应用程序的配置参数。
  • 购物车: 使用一个 Hash 存储用户的购物车信息,field 为商品 ID,value 为购买数量。例如,HSET cart:user:50 item:123 2 item:456 1
  • 统计: 存储对象的各种统计信息,例如文章的阅读数、点赞数、评论数等。HINCRBY article:stats:100 views 1

5.5 注意事项

  • 使用 Hash 存储对象相比于将对象序列化后存储为一个 String,主要优势在于可以单独获取或修改对象中的某个字段,而无需获取整个对象并反序列化。这在高并发场景下非常高效。
  • HGETALL 会获取哈希表的所有字段和值,对于字段非常多的 Hash,这可能是一个耗时且占用网络带宽的操作。如果只需要部分字段,使用 HMGET。对于非常大的 Hash,建议使用 HSCAN 进行迭代。
  • 尽管 Hash 内部使用了 ziplist 优化小哈希表,但当数据量增长超出阈值时,会转换为哈希表,此时每个字段都会有一定的内存开销。

结论

通过对 Redis 的五种基础数据类型的详细了解,我们可以看到 Redis 的强大之处在于它不仅仅是一个简单的键值存储系统,更是一个支持多种高效数据结构的平台。String 的通用性、List 的有序性和两端操作优势、Set 的唯一性和集合运算能力、Sorted Set 的排序和范围查询特性、以及 Hash 的结构化存储能力,使得 Redis 能够轻松应对缓存、队列、排行榜、计数器、发布/订阅、地理位置等多种复杂的应用场景。

选择正确的数据类型对于发挥 Redis 的最大性能和降低内存开销至关重要。在设计应用架构时,应仔细分析数据的特点和访问模式,选择最适合的 Redis 数据类型来存储和操作数据。熟练掌握这五种基础类型及其常用命令,是成为一名合格的 Redis 使用者的必经之路。

除了这五种基础类型,Redis 还提供了 Bitmaps(位图)、HyperLogLog(基数统计)、Geospatial(地理空间索引)等更高级的数据类型,它们在特定领域提供了更高效的解决方案。但掌握基础是前提,希望本文能为你深入学习 Redis 打下坚实的基础。


发表评论

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

滚动至顶部