Redis 核心数据结构、常用命令与过期策略

Redis 之所以能成为缓存与读写分离架构的首选,核心在于其丰富的数据结构。本章从底层原理出发,逐一拆解 String、Hash、List、Set、Sorted Set 五种核心类型,并覆盖通用命令与过期机制,为后续接口契约设计打下坚实基础。

2.1 五种核心数据结构总览

Redis 所有数据结构都以 key-value 形式存储,但 value 的内部编码与操作语义各不相同。下表从底层编码、最大元素数、典型应用三个维度进行对比:

数据结构 底层编码(主要) 单 key 最大元素数 典型应用场景
String int / embstr / raw 512 MB(值长度) 缓存对象、计数器、分布式锁
Hash ziplist / hashtable 2³² - 1 个 field 对象属性存储、用户信息
List quicklist 2³² - 1 个元素 消息队列、时间轴、最新列表
Set intset / hashtable 2³² - 1 个成员 标签系统、去重、交集/并集运算
Sorted Set ziplist / skiplist + dict 2³² - 1 个成员 排行榜、延时队列、范围查询
编码转换阈值(以 Redis 7.0 为例): Hash 使用 ziplist 时,field 数 ≤ 512 且每个 field 值 ≤ 64 字节;Sorted Set 使用 ziplist 时,成员数 ≤ 128 且每个成员值 ≤ 64 字节。超出阈值自动升级为 hashtable / skiplist。

2.2 String:最基础的缓存单元

String 是 Redis 最直接的数据类型,value 可以是字符串、整数或浮点数。在读写分离架构中,String 常用于存储序列化后的 JSON 对象或简单的计数器。

常用命令速查:

命令 语法 时间复杂度 说明
SET SET key value [NX|XX] [EX seconds] O(1) 设置键值,支持 NX(不存在时) / XX(存在时)
GET GET key O(1) 获取值,不存在返回 nil
INCR / DECR INCR key O(1) 原子自增/自减 1,用于计数器
MSET / MGET MSET key1 val1 key2 val2 O(N) 批量设置/获取,减少网络往返
GETSET GETSET key new_value O(1) 设置新值并返回旧值,用于原子更新
# 缓存用户信息(JSON 序列化)
SET user:1001 '{"name":"Alice","role":"admin"}' EX 3600
# 原子计数器
INCR page:view:home
# 分布式锁(NX + EX)
SET lock:order:12345 "thread-1" NX EX 30

2.3 Hash:结构化对象存储

Hash 适合存储一个对象的多个属性,相比 String 整体序列化,Hash 支持单独读写某个 field,减少网络传输量。在读写分离场景中,Hash 常用于缓存数据库行记录。

常用命令速查:

命令 语法 时间复杂度 说明
HSET HSET key field value O(1) 设置单个 field,存在则覆盖
HGET HGET key field O(1) 获取单个 field
HMSET / HMGET HMSET key f1 v1 f2 v2 O(N) 批量操作 field
HGETALL HGETALL key O(N) 返回所有 field-value,N 为 field 数
HINCRBY HINCRBY key field increment O(1) 原子增减 field 的整数值
# 缓存用户信息(按字段存储)
HSET user:2001 name "Bob" age 30 role "editor"
# 单独更新年龄
HINCRBY user:2001 age 1
# 获取全部字段(慎用于大 Hash)
HGETALL user:2001
HGETALL 性能陷阱: 当 Hash 的 field 数量超过 1000 时,HGETALL 会阻塞 Redis 单线程数毫秒。建议改用 HSCAN 分批迭代,或只读取需要的 field。

2.4 List:有序队列与消息缓冲

List 基于双向链表实现(quicklist 编码),支持从两端插入/弹出。在读写分离架构中,List 常用于异步写缓冲、任务队列或最新消息列表。

常用命令速查:

命令 语法 时间复杂度 说明
LPUSH / RPUSH LPUSH key value [value ...] O(1) 每元素 从左侧/右侧插入
LPOP / RPOP LPOP key [count] O(1) 从左侧/右侧弹出
LRANGE LRANGE key start stop O(S+N) 范围获取,S 为偏移量,N 为返回数
LLEN LLEN key O(1) 获取列表长度
LTRIM LTRIM key start stop O(N) 裁剪列表,只保留指定区间
# 消息队列:生产者
LPUSH msg:queue "task-001" "task-002"
# 消费者:阻塞弹出(超时 5 秒)
BRPOP msg:queue 5
# 最新 10 条记录
LPUSH news:latest "article-1" "article-2"
LTRIM news:latest 0 9

2.5 Set:去重与集合运算

Set 是无序、不可重复的集合,底层使用 intset(整数集合)或 hashtable。在读写分离中,Set 常用于标签去重、共同好友、权限交集等场景。

常用命令速查:

命令 语法 时间复杂度 说明
SADD SADD key member [member ...] O(1) 每成员 添加成员,已存在则忽略
SREM SREM key member [member ...] O(1) 每成员 移除成员
SMEMBERS SMEMBERS key O(N) 返回所有成员(慎用于大 Set)
SINTER SINTER key [key ...] O(N*M) 交集运算,N 为最小 Set 大小
SUNION SUNION key [key ...] O(N) 并集运算
SCARD SCARD key O(1) 获取成员数量
# 用户标签
SADD user:3001:tags "tech" "redis" "backend"
# 共同标签(交集)
SINTER user:3001:tags user:3002:tags
# 随机弹出成员(用于抽奖)
SPOP lottery:pool 3

2.6 Sorted Set:带权重的有序集合

Sorted Set 每个成员关联一个 double 类型的 score,按 score 排序。底层使用 skiplist + dict 实现,读写分离架构中常用于排行榜、延时队列、范围分页查询。

常用命令速查:

命令 语法 时间复杂度 说明
ZADD ZADD key score member [score member ...] O(log N) 每成员 添加或更新成员分数
ZRANGE / ZREVRANGE ZRANGE key start stop [WITHSCORES] O(log N + M) 按分数升序/降序范围获取
ZRANK / ZREVRANK ZRANK key member O(log N) 获取成员排名(升序/降序)
ZSCORE ZSCORE key member O(1) 获取成员分数
ZINCRBY ZINCRBY key increment member O(log N) 原子增减分数
ZREMRANGEBYSCORE ZREMRANGEBYSCORE key min max O(log N + M) 按分数范围删除
# 游戏排行榜
ZADD leaderboard:game1 1000 "player_A" 850 "player_B"
# 获取前三名(降序)
ZREVRANGE leaderboard:game1 0 2 WITHSCORES
# 玩家分数 +50
ZINCRBY leaderboard:game1 50 "player_A"
# 延时队列:以时间戳为 score
ZADD delay:queue 1700000000 "task-001"
ZADD delay:queue 1700000100 "task-002"
# 取出已到期的任务
ZRANGEBYSCORE delay:queue 0 1700000005

2.7 通用命令与键管理

无论使用哪种数据结构,以下通用命令在读写分离接口中频繁出现:

命令 语法 时间复杂度 说明
EXISTS EXISTS key [key ...] O(1) 判断键是否存在
DEL DEL key [key ...] O(N) 删除键,N 为键数量
EXPIRE EXPIRE key seconds O(1) 设置生存时间(秒)
TTL TTL key O(1) 查看剩余生存时间(秒)
TYPE TYPE key O(1) 返回数据类型
RENAME RENAME key newkey O(1) 重命名键
# 检查缓存是否存在
EXISTS user:1001
# 设置过期时间(秒)
EXPIRE session:token_abc 7200
# 查看剩余时间
TTL session:token_abc
# 删除缓存
DEL user:1001 user:1002

2.8 过期策略:被动删除与主动清理

Redis 的过期机制直接影响缓存一致性。理解其工作原理,才能设计出可靠的读写分离缓存策略。

三种过期删除策略:

  1. 被动删除(惰性删除): 当客户端访问一个 key 时,Redis 检查其是否过期,若过期则删除并返回 nil。这是最基础的删除方式,不占用额外 CPU,但过期 key 可能长时间占用内存。
  2. 主动删除(定期删除): Redis 每秒执行 10 次(hz 参数可配)的定时任务,随机抽取 20 个设置了过期时间的 key,删除其中已过期的 key。如果过期比例超过 25%,则重复抽取。这种方式平衡了 CPU 与内存。
  3. 内存淘汰(逐出策略): 当内存达到 maxmemory 限制时,Redis 根据配置的策略(如 allkeys-lru、volatile-ttl)主动淘汰 key。即使 key 未过期,也可能被逐出。
读写分离中的过期设计原则:
  • 读缓存设置 TTL 时,建议加上随机偏移(±10%~20%),防止大量缓存同时过期导致缓存雪崩。
  • 写操作更新数据库后,应主动删除或更新缓存,而不是依赖过期被动失效。
  • 对于热点 key,可设置较长的 TTL 并配合后台异步刷新,避免缓存击穿。
# 设置缓存时加入随机 TTL(伪代码)
int baseTTL = 3600;
int randomOffset = (int)(Math.random() * 600) - 300;  // ±5分钟
SETEX user:1001 3600 (baseTTL + randomOffset) serializedData

2.9 数据结构选型决策流程

在读写分离接口设计中,选择合适的数据结构能显著提升性能。以下 SVG 流程图帮助快速决策:

Redis 数据结构选型决策流程 数据需要缓存? 是否需要读写单个字段? 是 Hash 否 是否需要排序? 是 Sorted Set 否 是否需要去重? 是 Set 否 是否需要队列? 是 List 否 String

2.10 本章小结