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]
:设置键的值。EX
和PX
设置过期时间,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
生产者将消息推入列表,消费者使用RPOP
或BLPOP
从列表尾部取出消息(或反过来使用RPUSH
/LPOP
)。BLPOP
和BRPOP
实现阻塞等待,非常适合构建简单的消息队列。 - 栈: 使用
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 执行
LINDEX
或LRANGE
获取大量元素的慢操作,可能阻塞 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,
ZRANGE
或ZREVRANGE
如果返回大量元素,可能会占用较多网络带宽和客户端内存。可以使用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 打下坚实的基础。