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 个成员 | 排行榜、延时队列、范围查询 |
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
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 的过期机制直接影响缓存一致性。理解其工作原理,才能设计出可靠的读写分离缓存策略。
三种过期删除策略:
- 被动删除(惰性删除): 当客户端访问一个 key 时,Redis 检查其是否过期,若过期则删除并返回 nil。这是最基础的删除方式,不占用额外 CPU,但过期 key 可能长时间占用内存。
- 主动删除(定期删除): Redis 每秒执行 10 次(hz 参数可配)的定时任务,随机抽取 20 个设置了过期时间的 key,删除其中已过期的 key。如果过期比例超过 25%,则重复抽取。这种方式平衡了 CPU 与内存。
- 内存淘汰(逐出策略): 当内存达到 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 流程图帮助快速决策: