Skip to content

五种数据类型的典型业务用法

本文是 Redis 系统学习系列的 L1 入门篇。前置:27. Redis 入门:从安装到第一个缓存。 学完可以配合面试题食用:01-data-structures-encoding

为什么数据类型重要

Redis 能撑起缓存、计数、社交、排行榜那么多场景,底层靠的就是五种基本数据类型。每个类型不是简单的"存什么读什么",而是携带了特定的操作指令,能让你用一条命令完成一个业务逻辑,不用把数据拉到客户端算。

举个例子:你要个排行榜,用 MySQL 写 ORDER BY score DESC LIMIT 10 每次查一遍,数据量大了就不行。Redis 的 ZSet 用 ZREVRANGE 一行命令搞定,O(logN) 复杂度。选对类型,少写代码还快。

String:不只是存字符串

String 是 Redis 最基础的类型,value 最大 512MB,但实际应该控制在几百 KB 以下。

计数器

INCR key 自增 1,INCRBY key n 加 n。原子操作,天然适合限流计数器:

bash
# 接口限流:每分钟 IP 最多 100 次
SET ip:192.168.1.1:rate:20250823-09 0 EX 60 NX
INCR ip:192.168.1.1:rate:20250823-09
# 返回 1..100 正常,超过表示需要限流

分布式锁(初版)

SET key value NX EX 30 是分布式锁的基础版。NX 保证只有 key 不存在时才设置成功,EX 30 是自动过期防死锁。生产环境还需要 value 放唯一标识(防误删)和 Lua 脚本保证原子释放,但最核心的就是这一行。

位图签到

SETBIT user:1001:sign:202508 3 1 把第 3 位(签到日)置 1。BITCOUNT 统计总签到天数,GETBIT 查某天是否签到。一个用户一年的签到记录只占 46 字节(365 位),比存一个字符串列表省两个数量级。

什么不该用 String 做:存大 JSON 对象,尤其是频繁改部分字段的场景。比如用户信息有 20 个字段,每次改一个都要整个序列化/反序列化。这时候应该用 Hash。

Hash:对象存储的首选

HSET user:1001 name "张三" age 28 city "北京" 把用户信息存成一个 Hash,操作字段互相独立。

购物车

bash
# 用户 1001 的购物车
HSET cart:1001 sku:1001 2   # 商品 1001,数量 2
HSET cart:1001 sku:1002 1
HGETALL cart:1001           # 一次取全部商品和数量
HDEL cart:1001 sku:1001     # 移除商品
HLEN cart:1001              # 商品种类数

Hash vs JSON String 的取舍

对比项HashJSON String
单字段读写是,HGET/HSET否,必须整个取出再反序列化
内存效率略高(小字段用 ziplist 编码)取决于序列化库
过期时间仅 key 级仅 key 级
嵌套结构不支持支持

选型规则:如果经常要读写对象的部分字段,用 Hash;如果多数时候是整体存取,或者对象字段数不确定/嵌套深,用 JSON String 更省事。

什么不该用 Hash 做:字段数超过 5000 的 Hash 会成为 bigkey,HGETALL 一次拉出几 MB 数据会让 Redis 线程卡住。大 Hash 应该拆分成多个小 Hash,按 field_hash % N 分桶。

List:最新动态与简易队列

LPUSH + LTRIM 实现最新 N 条

bash
# 用户最新 100 条动态
LPUSH feed:user:1001 "动态内容3"
LPUSH feed:user:1001 "动态内容2"
# 每次 LPUSH 后 trim 只保留前 100 条
LTRIM feed:user:1001 0 99
# 取最新 10 条
LRANGE feed:user:1001 0 9

LTRIM 是 O(N) 操作(N 是删除的元素数),但每次只删多出的一条,实际代价很低。

简易队列

bash
# 生产者
LPUSH task:queue "job1"
# 消费者,阻塞等待
BRPOP task:queue 0

BRPOP 阻塞式弹出,实现了一个最简单的队列。但注意:List 不适合做可靠消息队列。消息被 BRPOP 消费后立即从 List 删除,如果消费者拿到消息后处理失败,消息就丢了。没有 ACK 机制、没有消息重投、没有死信处理。正规场景用 Redis Stream(5.0+)或专门的 MQ。

什么不该用 List 做:可靠消息队列、需要 ACK 的工作流、消息去重。

Set:去重与集合运算

共同关注

bash
SADD user:1001:follow "user:2001" "user:2002" "user:2003"
SADD user:1002:follow "user:2001" "user:2004"
# 共同关注
SINTERSTORE common:1001:1002 user:1001:follow user:1002:follow
# 结果:user:2001

SINTERSTORE 把交集结果存入新 key,适合"共同好友"这类场景。如果只是判断是否存在,用 SISMEMBER key member,O(1)。

抽奖

bash
# 参与抽奖的用户
SADD lottery:20250823 "user:1001" "user:1002" "user:1003" "user:1004"
# 抽 1 个奖
SPOP lottery:20250823  # 随机弹出(不重复)
# 或者抽完还保留参与者(如显示中奖名单)
SRANDMEMBER lottery:20250823 3  # 随机取 3 个,不删除

去重

bash
# 记录某篇文章的点赞用户
SADD article:2001:like "user:1001"
SADD article:2001:like "user:1002"
SADD article:2001:like "user:1001"  # 重复,忽略
SCARD article:2001:like  # 返回 2

什么不该用 Set 做:带权重的集合(用 ZSet)、需要保持插入顺序的列表(用 List)。

ZSet:排行榜与时间窗口

排行榜

bash
# 玩家积分变化
ZINCRBY game:leaderboard 150 "player:1001"  # 加 150 分
ZINCRBY game:leaderboard 200 "player:1002"
# 前 10 名
ZREVRANGE game:leaderboard 0 9 WITHSCORES
# 查某玩家排名
ZREVRANK game:leaderboard "player:1001"
# 查某玩家分数
ZSCORE game:leaderboard "player:1001"

ZINCRBY 是原子自增,不用担心并发加分冲突。ZREVRANGE 按分数从高到低取,O(logN + M) 复杂度,百万级数据随便查。

滑动窗口限流

bash
# 用户 ID 1001,每分钟最多 5 次请求
# 用当前时间戳(毫秒)作为 score
ZADD rate:user:1001:20250823-09 1725312000000 "req:1"
ZADD rate:user:1001:20250823-09 1725312001000 "req:2"
# 移除 60 秒前的记录
ZREMRANGEBYSCORE rate:user:1001:20250823-09 -inf 1725311940000
# 统计当前窗口内请求数
ZCARD rate:user:1001:20250823-09

这个方案在时间窗口内的请求数精确、无毛刺,但每个用户每分钟要写一条 ZADD。如果 QPS 很高,ZSet 的 ZREMRANGEBYSCORE 会 O(logN) 删除,对内存也有压力。高并发场景下更推荐用令牌桶或本地滑动窗口,不做精确计数。

什么不该用 ZSet 做:纯 FIFO 队列(用 List)、不需要排序的唯一集合(用 Set)。

动手实操

以下用 redis-cli 演示每个类型的一个典型场景:

bash
# 1. String 计数器
127.0.0.1:6379> INCR site:visit:total
(integer) 1
127.0.0.1:6379> INCR site:visit:total
(integer) 2

# 2. Hash 用户信息
127.0.0.1:6379> HSET user:1001 name "Alice" level "VIP"
(integer) 2
127.0.0.1:6379> HGET user:1001 name
"Alice"
127.0.0.1:6379> HINCRBY user:1001 score 50   # 分数加 50
(integer) 50

# 3. List 最新动态
127.0.0.1:6379> LPUSH news:hot "news3" "news2" "news1"
(integer) 3
127.0.0.1:6379> LTRIM news:hot 0 2
OK
127.0.0.1:6379> LRANGE news:hot 0 -1
1) "news3"
2) "news2"
3) "news1"

# 4. Set 去重
127.0.0.1:6379> SADD tags:article:99 "java" "redis" "spring" "java"
(integer) 3       # 重复的 "java" 只算一次
127.0.0.1:6379> SMEMBERS tags:article:99
1) "redis"
2) "spring"
3) "java"

# 5. ZSet 排行榜
127.0.0.1:6379> ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
(integer) 3
127.0.0.1:6379> ZREVRANGE leaderboard 0 1 WITHSCORES
1) "player2"
2) "200"
3) "player3"
4) "150"

常见误区与小结

  • 误区:String 存所有。能用 Hash 存对象字段就尽量用 Hash,避免频繁序列化整个对象。
  • 误区:List 做可靠队列。List 没有 ACK 机制,消费失败消息就丢了。需要可靠消费用 Stream。
  • 误区:ZSet 的 score 只存整数。score 是 64 位浮点数,可以存时间戳、分数、甚至组合的整数编码(如 时间戳<<32 | 自增ID)。
  • 误区:SADD 重复不报错是正常的。Set 天然去重,重复插入返回 0,不是 bug。
  • 误区:单 key 存几万条数据没问题。一个 ZSet 百万级成员、一个 Hash 几千字段都会变成 bigkey,影响集群迁移和查询性能。

小结:五种数据类型各有适用场景,选型依据是"业务操作模型"而不是"数据长什么样"。String 适合计数和锁,Hash 适合对象,List 适合列表和简易队列,Set 适合去重和集合运算,ZSet 适合排序。下一篇数据结构拆解:SDS、Dict、Listpack 与 Skiplist 会深入底层,看这些类型在内部是怎么存储的。

参考

参考:Redis 官方文档 Data Types — 每个类型都有交互式教程 Redis 源码 src/t_string.csrc/t_hash.csrc/t_list.csrc/t_set.csrc/t_zset.c — 各类型的命令实现入口

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1