2. Redis 核心数据结构:String、Hash、List、Set、Sorted Set 的使用场景与命令

Redis 之所以能成为缓存与搜索闭环中的核心组件,很大程度上归功于其丰富的数据结构。理解这五种基础结构的特性、适用场景及关键命令,是实现缓存一致性与高性能查询的基石。本章将逐一剖析每种结构,并结合实战场景给出选型建议。

2.1 数据结构全景对比

在深入细节之前,先通过一张对比表格建立全局认知,明确每种结构的核心特征与典型用途。

数据结构 底层实现 最大元素数 核心特性 典型场景
String 动态字符串 (SDS) 512 MB 最基础、二进制安全、可自增/自减 缓存对象、计数器、分布式锁
Hash 压缩列表 / 哈希表 2^32 - 1 个 field 结构化存储、支持单独操作字段 用户信息、商品详情、会话管理
List 压缩列表 / 双向链表 2^32 - 1 个元素 有序、可重复、支持双端操作 消息队列、时间线、最新消息
Set 哈希表 / 整数集合 2^32 - 1 个元素 无序、唯一、支持集合运算 标签系统、去重、共同好友
Sorted Set 压缩列表 / 跳表 + 哈希表 2^32 - 1 个元素 有序、唯一、按 score 排序 排行榜、延时队列、范围查询

2.2 String:最通用的缓存载体

String 是 Redis 中最简单的数据结构,但也是使用频率最高的。它不仅可以存储文本,还能存储序列化后的二进制对象(如图片、JSON 字符串)。在缓存一致性场景中,String 常用于存储单个实体的完整快照。

关键命令

  • SET / GET:基础读写,支持 NX/XX 选项实现分布式锁。
  • MSET / MGET:批量操作,减少网络往返。
  • INCR / DECR:原子自增/自减,适用于计数器。
  • SETEX / PSETEX:设置值的同时指定过期时间,是缓存失效策略的基础。
  • GETSET:原子地获取旧值并设置新值,常用于实现限流或轮询。

实战场景

  • 缓存数据库查询结果:将 SQL 查询结果序列化为 JSON 存入 String,配合 TTL 实现自动失效。
  • 分布式锁:使用 SET key value NX EX 10 实现互斥锁,防止缓存击穿。
  • 计数器:记录文章阅读量、点赞数,利用 INCR 保证原子性。
缓存一致性提示: 使用 String 缓存对象时,建议将 TTL 设置为业务可接受的最大延迟时间。更新数据库后,通过 DEL 或 SET 主动失效缓存,避免脏数据长期存在。

2.3 Hash:结构化对象的理想选择

Hash 允许你在一个 key 下存储多个 field-value 对,非常适合表示对象。与 String 存储整个 JSON 相比,Hash 支持单独读写某个字段,节省带宽并减少序列化开销。

关键命令

  • HSET / HGET:设置/获取单个字段。
  • HMSET / HMGET:批量操作多个字段。
  • HGETALL:获取所有字段和值,注意数据量大时可能阻塞。
  • HINCRBY:对某个字段执行原子自增。
  • HDEL:删除一个或多个字段。
  • HEXISTS:检查字段是否存在。

实战场景

  • 用户信息缓存:以用户 ID 为 key,字段为 name、email、age 等,可单独更新某个属性。
  • 商品详情:存储商品的标题、价格、库存,库存变更时只需更新一个字段。
  • 会话管理:存储用户登录态、权限信息,支持局部更新。
选型建议: 当对象字段经常需要部分更新时,优先选择 Hash 而非 String。例如用户修改昵称,Hash 只需 HSET user:1001 nickname "新名字",而 String 需要读取整个 JSON、反序列化、修改、再序列化写入。

2.4 List:有序队列与时间线

List 是一个有序的字符串列表,支持从两端插入和弹出元素。它的底层实现会根据元素数量和大小在压缩列表与双向链表之间自动切换。

关键命令

  • LPUSH / RPUSH:从左侧/右侧插入元素。
  • LPOP / RPOP:从左侧/右侧弹出元素。
  • LRANGE:获取指定范围内的元素,常用于分页。
  • LTRIM:截取列表,保留指定范围内的元素。
  • BLPOP / BRPOP:阻塞式弹出,用于实现消息队列。

实战场景

  • 消息队列:生产者使用 LPUSH,消费者使用 BRPOP,实现简单的异步解耦。
  • 最新动态/时间线:使用 LPUSH 插入新内容,LTRIM 保留最近 N 条。
  • 日志收集:多个进程向同一个 List 写入日志,后台进程批量读取处理。
注意事项: List 不适合存储大量数据(超过数万条)后进行随机访问,LRANGE 的时间复杂度为 O(N)。如果需要频繁的中间元素访问,请考虑 Sorted Set 或使用其他中间件。

2.5 Set:标签与去重利器

Set 是一个无序的、元素唯一的集合。它的核心价值在于支持高效的集合运算(交集、并集、差集),非常适合处理标签、分类和关系数据。

关键命令

  • SADD / SREM:添加/移除元素。
  • SMEMBERS:获取所有元素(慎用于大集合)。
  • SISMEMBER:判断元素是否存在,时间复杂度 O(1)。
  • SINTER / SUNION / SDIFF:交集、并集、差集运算。
  • SCARD:获取集合基数(元素数量)。
  • SPOP / SRANDMEMBER:随机弹出/获取元素。

实战场景

  • 标签系统:为文章打标签,通过 SINTER 找出同时包含多个标签的文章。
  • 共同好友/关注:使用 SINTER 计算两个用户的共同关注。
  • 去重统计:记录某活动的独立访客 IP,使用 SADD 自动去重。

2.6 Sorted Set:排行榜与范围查询

Sorted Set 在 Set 的基础上为每个元素关联了一个 double 类型的 score,元素按 score 排序。它结合了跳表与哈希表的优势,既能快速查找元素,又能高效执行范围查询。

关键命令

  • ZADD / ZREM:添加/移除元素,ZADD 可同时更新 score。
  • ZRANGE / ZREVRANGE:按 score 升序/降序获取指定范围内的元素。
  • ZRANK / ZREVRANK:获取元素的排名(从 0 开始)。
  • ZSCORE:获取元素的 score。
  • ZINCRBY:对元素的 score 执行原子增减。
  • ZRANGEBYSCORE:按 score 范围获取元素,常用于分页或时间范围查询。
  • ZINTERSTORE / ZUNIONSTORE:交集/并集运算并存储结果。

实战场景

  • 实时排行榜:以用户 ID 为元素,积分或分数为 score,使用 ZREVRANGE 获取 Top N。
  • 延时队列:以任务 ID 为元素,执行时间戳为 score,使用 ZRANGEBYSCORE 获取到期任务。
  • 滑动窗口限流:以时间戳为 score,统计最近 N 秒内的请求数。
缓存一致性提示: 在排行榜场景中,更新数据库中的分数后,应立即使用 ZINCRBY 或 ZADD 同步更新 Redis 中的 score。如果允许短暂不一致,可设置定时任务批量同步,但需注意排行榜的实时性要求。

2.7 数据结构选型决策流程

下面的 SVG 流程图总结了在实际业务中如何根据数据特征和操作模式选择最合适的 Redis 数据结构。

Redis 数据结构选型决策流程 数据需要缓存? 是否需要排序? 是 Sorted Set 否 需要唯一性? 是 Set 否 需要双端操作? 是 List 否 需要部分更新? 是 Hash 否 String 决策依据 排序 → Sorted Set 唯一 → Set

2.8 命令复杂度速查表

在编写缓存代码时,了解命令的时间复杂度至关重要,避免因误用导致性能瓶颈。下表列出了常用命令的复杂度等级。

数据结构 命令 时间复杂度 注意事项
String SET / GET O(1) 最基础操作,性能极高
MSET / MGET O(N) N 为 key 的数量,批量操作减少 RTT
INCR / DECR O(1) 原子操作,适合高并发计数
Hash HSET / HGET O(1) 单字段操作
HGETALL O(N) N 为 field 数量,大对象慎用
HINCRBY O(1) 原子增减字段值
List LPUSH / RPOP O(1) 双端操作,性能稳定
LRANGE O(N) N 为返回元素数量,避免取全部
LTRIM O(N) N 为被移除元素数量
Set SADD / SREM O(1) 单元素操作
SISMEMBER O(1) 高效判断存在性
SINTER / SUNION O(N * M) N 为最小集合大小,M 为集合数
Sorted Set ZADD / ZREM O(log N) 跳表插入/删除
ZRANGE / ZREVRANGE O(log N + M) M 为返回元素数量
ZRANGEBYSCORE O(log N + M) 按 score 范围查询

2.9 实战代码示例:缓存用户信息

以下示例展示了如何使用 Hash 结构缓存用户信息,并实现缓存与数据库的一致性更新。

// 伪代码:缓存用户信息,使用 Hash 结构
public User getUserById(String userId) {
    // 1. 尝试从 Redis 缓存获取
    Map<String, String> userMap = redis.hgetAll("user:" + userId);
    if (userMap != null && !userMap.isEmpty()) {
        return mapToUser(userMap);
    }

    // 2. 缓存未命中,从数据库查询
    User user = database.getUserById(userId);
    if (user == null) {
        return null;
    }

    // 3. 写入缓存,设置 TTL 为 1 小时
    Map<String, String> cacheMap = userToMap(user);
    redis.hmset("user:" + userId, cacheMap);
    redis.expire("user:" + userId, 3600);

    return user;
}

// 更新用户信息,保证缓存一致性
public void updateUser(User user) {
    // 1. 先更新数据库
    database.updateUser(user);

    // 2. 同步更新缓存(或删除缓存)
    Map<String, String> cacheMap = userToMap(user);
    redis.hmset("user:" + user.getId(), cacheMap);
    redis.expire("user:" + user.getId(), 3600);
    // 或者使用 redis.del("user:" + user.getId()) 删除缓存,下次读取时重建
}
最佳实践: 在缓存一致性要求较高的场景中,推荐使用 先更新数据库,再删除缓存 的策略。这样可以避免并发更新时出现缓存与数据库不一致的问题。如果业务允许短暂的不一致,也可以选择先更新缓存,再异步更新数据库。

2.10 本章小结

Redis 的五种核心数据结构各有侧重,选型时应紧扣业务需求:

  • String 是万能的缓存载体,适合简单键值对和计数器。
  • Hash 是结构化对象的首选,支持部分更新。
  • List 擅长处理有序队列和时间线。
  • Set 是去重和集合运算的利器。
  • Sorted Set 是排行榜和范围查询的最佳选择。