RedisNotes

第 05 章:五大基础类型

zjc 于 2026-01-05 发布

这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 数据结构选型决定性能、内存与代码复杂度。本章深入 String、Hash、List、Set、ZSet 的命令、语义与典型场景。

5.1 String:不止字符串

String 可以保存:

常用命令

SET user:1001 "Tom" EX 3600
SETNX user:1001 "Tom"
GET user:1001
GETRANGE user:1001 0 2
SETRANGE user:1001 0 "ABC"
MSET a 1 b 2
MGET a b

INCR read:article:1
INCRBY stock:sku:1 -5
INCRBYFLOAT price:sku:1 12.5

缓存对象

SET shop:product:10001 '{"id":10001,"title":"Phone","price":4999}' EX 600

优点:一次读写完整对象,实现简单。缺点:更新任意字段都要读改写整个 JSON,序列化成本随对象变大。

计数器

INCR counter:page:1001
INCRBY counter:page:1001 10
DECR counter:stock:sku:1

计数操作原子,适合点赞、阅读、库存、限额。注意计数器的初始值与下限校验,避免超卖:

DECR stock:sku:1

如果原值为 0,会变成 -1。库存扣减应结合 Lua 或 DECR 后判断回滚,第 12 章给出完整方案。

分布式锁基础

SET lock:order:1001 owner-uuid NX PX 30000

一个命令同时完成不存在才写入和 TTL,避免命令交错。完整锁方案见第 11 章。

5.2 Hash:对象与字段映射

Hash 适合对象属性、购物车、配置项:

HSET user:1001 name Tom age 28 city Beijing
HGET user:1001 name
HMGET user:1001 name age
HGETALL user:1001
HLEN user:1001
HEXISTS user:1001 age
HINCRBY user:1001 age 1
HDEL user:1001 city

局部更新

更新昵称:

HSET user:1001 name Jerry

不需要读取和重写整个对象,也避免并发覆盖其他字段。

购物车

HSET cart:user:1001 sku:2001 2
HINCRBY cart:user:1001 sku:2001 1
HDEL cart:user:1001 sku:2001
HGETALL cart:user:1001

field 是 SKU,value 是数量,天然可增量修改。

HSCAN

HSCAN user:events:1001 0 MATCH click:* COUNT 100

大 Hash 不要 HGETALL 全量读取,应分批扫描或按业务拆分。

5.3 List:双端列表

List 是有序、可重复的序列,支持两端推入弹出:

LPUSH queue:task a b c
RPUSH queue:task x y
LRANGE queue:task 0 -1
LPOP queue:task
RPOP queue:task
LLEN queue:task
LINDEX queue:task 0
LSET queue:task 0 new-value
LREM queue:task 1 value
LTRIM queue:task -100 -1

简单队列

LPUSH task:queue task-id
BRPOP task:queue 5

BRPOP/BLPOP 会阻塞等待,避免客户端空转。但如果要求 ack、消费组、pending 重试,应使用 Stream。

最新列表

LPUSH feed:following:user:1001 post:9001
LTRIM feed:following:user:1001 0 999
LRANGE feed:following:user:1001 0 20

保持固定长度,避免 List 无限膨胀。

5.4 Set:无序唯一集合

SADD user:1001:tags java redis distributed-system
SISMEMBER user:1001:tags redis
SREM user:1001:tags java
SCARD user:1001:tags
SMEMBERS user:1001:tags
SRANDMEMBER user:1001:tags 2
SPOP user:1001:tags

集合运算

SADD user:1001:follow u1 u2 u3
SADD user:2002:follow u2 u3 u4

SINTER user:1001:follow user:2002:follow   # 共同关注
SUNION user:1001:follow user:2002:follow   # 全部关注
SDIFF user:1001:follow user:2002:follow    # 我关注但他未关注

去重

活动参与用户:

SADD activity:2026:users user:1001
SISMEMBER activity:2026:users user:1001
SCARD activity:2026:users

精确去重内存较大。若只要求估算 UV,HyperLogLog 更合适。

5.5 ZSet:有序集合

ZSet 的 member 唯一,每个 member 关联 score,按 score 排序:

ZADD rank:activity:2026 100 user:1001 200 user:2002
ZSCORE rank:activity:2026 user:1001
ZINCRBY rank:activity:2026 50 user:1001
ZRANGE rank:activity:2026 0 9 WITHSCORES
ZREVRANGE rank:activity:2026 0 9 WITHSCORES
ZRANGEBYSCORE rank:activity:2026 (100 200 WITHSCORES LIMIT 0 10
ZRANK rank:activity:2026 user:1001
ZREVRANK rank:activity:2026 user:1001
ZREM rank:activity:2026 user:1001

排行榜

ZINCRBY rank:game 10 player:1001
ZREVRANGE rank:game 0 9 WITHSCORES
ZREVRANK rank:game player:1001

同分排序需要额外规则,可以把时间戳或自定义序号编码进 score,例如:

score = 分数 * 10^10 + (MAX_TIME - 秒级时间戳)

读取后再拆分,注意精度边界。

延迟队列

ZADD delay:tasks 1735000000 task:1001
ZRANGEBYSCORE delay:tasks 0 <now> LIMIT 0 100
ZREM delay:tasks task:1001

只有成功 ZREM 的任务才执行,避免多消费者重复处理。

5.6 类型选择决策表

需求 推荐类型
缓存完整只读对象 String(JSON)
对象需要局部字段更新 Hash
计数器/库存数 String
双端队列/最新列表 List
去重集合/标签/交并差 Set
按分数排序/范围查询 ZSet
签到/活跃位标记 Bitmap
海量 UV 估算 HyperLogLog
附近位置 GEO
消息队列 Stream

5.7 类型混用与迁移

同一个 key 只能有一种 value 类型:

SET user:1001 "Tom"
LPUSH user:1001 a
# WRONGTYPE Operation against a key holding the wrong kind of value

结构变更推荐:

  1. 新 key 使用新版本前缀:user:1001:v2
  2. 双写或后台迁移;
  3. 读新失败读旧;
  4. 校验数量与抽样值;
  5. 稳定后下线旧 key。

不要在线上直接边删边重建,除非业务明确允许丢失。

本章小结

思考题

  1. 商品详情页缓存用 String(JSON) 和 Hash 各有什么优缺点?
  2. 为什么延迟队列推荐 ZSet 而不是 List?
  3. 设计一个“共同好友 + 好友数量排行榜”,需要组合哪些 Set/ZSet 命令?