引言:Redis 为何如此之快?数据结构是关键
Redis (Remote Dictionary Server) 作为一款高性能的内存键值数据库,凭借其闪电般的速度和丰富的功能,在缓存、消息队列、排行榜等众多场景中得到了广泛应用。除了基于内存存储这一核心优势外,Redis 高效的底层数据结构设计是其高性能的关键所在。
理解 Redis 的核心数据结构,不仅能帮助我们更高效地使用 Redis,还能在面对复杂业务场景时,做出更优的技术选型。本文将带你深入了解 Redis 最常用的五种基本数据结构及其内部实现,并探讨它们的典型应用场景。
一、字符串 (String):最基础也最万能
字符串是 Redis 最基础的数据类型,也是其他复杂数据结构的基础。但 Redis 的字符串并非 C 语言中的普通字符串 (char*),而是自己构建的一种名为 简单动态字符串 (Simple Dynamic String, SDS) 的结构。
-
内部实现: SDS 结构包含 len (已用长度)、alloc (已分配空间) 和 buf (字节数组) 等字段。
- O(1) 获取长度: len 字段使得获取字符串长度的时间复杂度为 O(1)。
- 杜绝缓冲区溢出: alloc 字段记录了已分配空间,修改 SDS 时会检查空间是否足够,不足则先扩展,避免了 C 字符串常见的溢出风险。
- 减少内存重分配次数: SDS 采用空间预分配(修改后如果 len 小于 1MB,则分配 len*2 的空间;如果大于 1MB,则额外分配 1MB)和惰性空间释放(缩短字符串时不立即回收多余空间)策略,减少了内存重分配的开销。
- 二进制安全: SDS 使用 len 字段判断结束,而非空字符 \0,因此可以存储包含 \0 在内的任意二进制数据。
-
常用命令: SET, GET, INCR, DECR, STRLEN, APPEND, GETSET, MSET, MGET 等。
-
应用场景:
- 缓存: 缓存用户信息、配置信息、页面片段等。
- 计数器/限流器: INCR 命令原子性地增加数值,非常适合做计数器(如文章阅读量)或简单的限流控制。
- 分布式锁: 利用 SETNX (Set if Not Exists) 实现简单的分布式锁。
- 存储 Session 信息。
二、列表 (List):有序的队列与栈
Redis 的列表是一种有序的字符串集合,按照插入顺序排序。你可以在列表的头部(左边)或尾部(右边)添加元素。
-
内部实现:
- 在 Redis 3.2 之前,列表底层采用 双向链表 (Linked List) 或 压缩列表 (ZipList) 实现。当元素数量较少且每个元素大小不大时,使用 ZipList 以节省内存;否则使用 Linked List。
- 在 Redis 3.2 及之后,引入了 快速列表 (QuickList) 作为列表的底层实现。QuickList 是 Linked List 和 ZipList 的混合体,它将一个双向链表中的每个节点都设置为一个 ZipList,从而在空间效率和时间效率之间取得了更好的平衡。
-
特点: 允许存储重复元素,插入和删除操作快(头尾 O(1)),但索引定位较慢(O(N))。
-
常用命令: LPUSH, RPUSH, LPOP, RPOP, LRANGE, LINDEX, LLEN, BLPOP, BRPOP (阻塞式弹出) 等。
-
应用场景:
- 消息队列: 利用 LPUSH 生产消息,RPOP (或 BRPOP) 消费消息,实现简单的消息队列。
- 时间线/最新列表: 比如用户发布的最新微博、网站的最新动态等,使用 LPUSH 添加,LRANGE 获取。
- 栈: LPUSH + LPOP 或 RPUSH + RPOP 可以模拟栈的行为。
三、哈希 (Hash):结构化存储的利器
哈希类型(也称为散列或字典)是一个键值对集合,特别适合用于存储对象。可以看作是 Value 为 String 的 Map。
-
内部实现:
- 底层同样可以采用 压缩列表 (ZipList) 或 哈希表 (Hash Table) 实现。当键值对数量较少且键和值的大小不大时,使用 ZipList;否则使用 Hash Table。
- Hash Table 的实现方式与 java.util.HashMap 类似,采用数组 + 链表(或红黑树,在冲突过多时)的方式解决哈希冲突。Redis 的 Hash Table 在扩容/缩容时采用了渐进式 rehash,避免了单次 rehash 成本过高导致服务阻塞。
-
特点: 适合存储结构化数据,如一个对象的多个属性。可以对单个字段进行读写。
-
常用命令: HSET, HGET, HMSET, HMGET, HGETALL, HDEL, HLEN, HINCRBY, HEXISTS 等。
-
应用场景:
- 缓存对象信息: 存储用户信息(用户名、年龄、邮箱等)、商品信息(名称、价格、库存等)。相比于将对象序列化后用 String 存储,Hash 可以只修改或获取对象的某个字段,更灵活高效。
- 购物车: 用户 ID 为 Key,商品 ID 为 field,商品数量为 value。
四、集合 (Set):无序且唯一的元素
集合是 String 类型的无序集合,集合中的元素是唯一的,不允许重复。
-
内部实现:
- 底层采用 哈希表 (Hash Table) 或 整数集合 (IntSet) 实现。当所有元素都是整数且元素数量不多时,使用 IntSet 以节省内存;否则使用 Hash Table。
- 使用 Hash Table 实现时,所有 Value 都指向一个 NULL 指针,只利用 Key 来保证唯一性。
-
特点: 元素唯一、无序。支持高效的集合间运算(交集、并集、差集)。
-
常用命令: SADD, SREM, SMEMBERS, SISMEMBER, SCARD (获取基数), SINTER (交集), SUNION (并集), SDIFF (差集), SRANDMEMBER (随机获取元素) 等。
-
应用场景:
- 标签系统: 给用户或内容打标签。一个标签下的所有用户/内容 ID 可以存储在一个 Set 中。
- 共同好友/共同关注: 利用 SINTER 计算两个用户关注列表的交集。
- 抽奖系统: 使用 SADD 添加参与用户,SRANDMEMBER 或 SPOP 抽取中奖用户。
- 去重: 利用 Set 元素唯一的特性进行数据去重。
五、有序集合 (Sorted Set / ZSet):带权重的集合
有序集合和集合类似,也是 String 类型的元素的集合,且不允许重复。但不同的是,每个元素都会关联一个 double 类型的分数 (score)。Redis 正是通过分数来为集合中的成员进行从小到大的排序。
-
内部实现:
- 底层可以采用 压缩列表 (ZipList) 或 跳跃表 (Skip List) + 哈希表 (Hash Table) 的组合实现。当元素数量较少且元素和分数大小不大时,使用 ZipList;否则使用 Skip List + Hash Table。
- Skip List: 用于保证元素的有序性,支持平均 O(logN) 复杂度的查找、插入、删除操作,以及范围查找。
- Hash Table: 用于存储元素到分数的映射,保证 O(1) 复杂度获取指定元素的分数。两者结合,既能高效排序,又能快速查找单个元素。
-
特点: 元素唯一、有序(根据 score)。支持根据分数范围或元素字典序范围进行查找。
-
常用命令: ZADD, ZREM, ZRANGE (按分数范围查), ZREVRANGE (按分数倒序查), ZRANGEBYSCORE, ZCARD, ZSCORE, ZINCRBY, ZRANK (获取排名), ZREMRANGEBYSCORE 等。
-
应用场景:
- 排行榜: 如游戏积分榜、热门帖子排行等。ZADD 更新分数,ZRANGE 或 ZREVRANGE 获取排名。
- 带权重的消息队列: 可以用分数表示优先级或时间戳。
- 范围查找: 如查找某个价格区间或时间范围内的商品/数据。
Redis 6.0 及之后的新结构(简述)
除了上述五种基本结构,Redis 后续版本还引入了一些更专门化的数据结构:
- Bitmaps (位图): 底层是 String,但可以对位的级别进行操作。适合用于状态统计(如用户签到、活跃状态)。
- HyperLogLog: 基于概率的基数统计,占用空间极小(固定 12KB),用于计算海量数据的独立用户数等(允许一定误差)。
- Geospatial (GEO): 存储地理位置信息,并支持半径查询等操作。底层是 ZSet。
- Streams (流): Redis 5.0 引入的强大的消息队列模型,支持消费组、持久化、阻塞读取等。
如何选择合适的数据结构?
- 需要缓存简单键值对或进行计数 -> String
- 需要存储列表并保持顺序,如消息队列、时间线 -> List
- 需要存储结构化对象信息 -> Hash
- 需要存储唯一元素,进行集合运算或去重 -> Set
- 需要存储有序且唯一的元素,如排行榜 -> Sorted Set (ZSet)
- 需要进行大量的位操作或状态统计 -> Bitmap
- 需要对海量数据进行基数统计(允许误差) -> HyperLogLog
- 需要存储地理位置并进行范围查询 -> GEO
- 需要构建可靠的消息队列系统 -> Stream
结语
Redis 高效的底层数据结构是其高性能的核心秘密之一。深入理解 String (SDS)、List (QuickList)、Hash (HashTable/ZipList)、Set (HashTable/IntSet) 和 Sorted Set (SkipList+HashTable/ZipList) 的内部实现和特点,可以帮助我们更好地利用 Redis 的优势,设计出更健壮、更高效的应用程序。在实际应用中,根据具体的业务场景选择最合适的数据结构,是发挥 Redis 最大效能的关键。