这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 数据结构选型决定性能、内存与代码复杂度。本章深入 String、Hash、List、Set、ZSet 的命令、语义与典型场景。
5.1 String:不止字符串
String 可以保存:
- 普通文本;
- JSON/Protobuf 序列化对象; – 二进制数据(图片片段、序列化字节);
- 整数,用于
INCR/DECR; - 浮点数,用于
INCRBYFLOAT。
常用命令
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
结构变更推荐:
- 新 key 使用新版本前缀:
user:1001:v2; - 双写或后台迁移;
- 读新失败读旧;
- 校验数量与抽样值;
- 稳定后下线旧 key。
不要在线上直接边删边重建,除非业务明确允许丢失。
本章小结
- String 适合完整对象、计数、二进制与锁;
- Hash 适合多字段对象与局部更新;
- List 适合有序队列和最新列表,但可靠性弱于 Stream;
- Set 适合唯一元素与集合运算;
- ZSet 适合排序、范围查询与延迟任务;
- 大集合读取要分页/扫描,写入要评估元素数量和内存增长。
思考题
- 商品详情页缓存用 String(JSON) 和 Hash 各有什么优缺点?
- 为什么延迟队列推荐 ZSet 而不是 List?
- 设计一个“共同好友 + 好友数量排行榜”,需要组合哪些 Set/ZSet 命令?