Redis 数据类型选择指南
Redis 提供了多种数据类型,如何选择合适的数据类型是设计高效 Redis 应用的关键。本文将通过实际场景,帮助你选择最佳的数据类型。
一、数据类型总览
1.1 基础数据类型
Redis 数据类型体系:
┌─────────────────────────────────────┐
│ 基础类型 │
│ ├── String(字符串) │
│ ├── List(列表) │
│ ├── Set(集合) │
│ ├── Hash(哈希) │
│ └── ZSet(有序集合) │
├─────────────────────────────────────┤
│ 高级类型 │
│ ├── Bitmap(位图) │
│ ├── HyperLogLog(基数统计) │
│ ├── GEO(地理位置) │
│ └── Stream(流) │
└─────────────────────────────────────┘
1.2 性能对比
| 类型 | 时间复杂度 | 内存效率 | 适用场景 |
|---|---|---|---|
| String | O(1) | 高 | 缓存、计数器 |
| List | O(1) | 中 | 队列、栈 |
| Set | O(1) | 中 | 去重、集合运算 |
| Hash | O(1) | 高 | 对象存储 |
| ZSet | O(log N) | 低 | 排行榜 |
二、场景化选择
2.1 缓存场景
简单缓存 → String
# 用户信息缓存
SET user:1001:info '{"name":"张三","age":25}'
GET user:1001:info
# 商品详情缓存
SET product:12345:detail '{"title":"iPhone 15","price":7999}'
GET product:12345:detail
# 带过期时间
SET cache:article:98765 "文章内容..." EX 3600
对象缓存 → Hash
# 用户信息(分字段存储)
HSET user:1001 name "张三"
HSET user:1001 age 25
HSET user:1001 email "zhangsan@example.com"
# 获取单个字段(节省网络带宽)
HGET user:1001 name
# 获取全部
HGETALL user:1001
# 批量获取指定字段
HMGET user:1001 name email
Hash vs String 对比:
// 方案 1:String 存储整个对象
// 优点:简单、序列化一次
// 缺点:修改单个字段需要获取整个对象
String key = "user:1001";
String json = objectMapper.writeValueAsString(user);
redis.set(key, json);
// 方案 2:Hash 分字段存储
// 优点:可单独修改字段、节省内存
// 缺点:需要多次操作
String key = "user:1001";
redis.hset(key, "name", user.getName());
redis.hset(key, "age", String.valueOf(user.getAge()));
// 内存对比(100 万用户)
// String: ~150MB(包含 JSON 开销)
// Hash: ~120MB(压缩列表优化)
2.2 计数器场景
简单计数 → String (INCR)
# 文章阅读量
INCR article:12345:views
# 视频播放量
INCR video:67890:plays
# 商品销量
INCR product:11111:sales
# 带初始值
SETNX counter:unique:visitors 0
INCR counter:unique:visitors
多维度计数 → Hash
# 商品多维度统计
HINCRBY product:12345:stats views 1 # 浏览量
HINCRBY product:12345:stats favorites 1 # 收藏数
HINCRBY product:12345:stats shares 1 # 分享数
# 获取所有统计
HGETALL product:12345:stats
位统计 → Bitmap
# 日活统计(每个用户 1 bit)
SETBIT dau:2025-09-05 user_id 1
# 统计日活人数
BITCOUNT dau:2025-09-05
# 多日对比(交集、并集)
BITOP AND result dau:day1 dau:day2 dau:day3
BITCOUNT result
基数统计 → HyperLogLog
# UV 统计(独立访客)
PFADD uv:2025-09-05 user_id_1
PFADD uv:2025-09-05 user_id_2
PFADD uv:2025-09-05 user_id_1 # 重复添加不计入
# 统计 UV
PFCOUNT uv:2025-09-05
# 合并多天 UV
PFMERGE uv:total uv:day1 uv:day2 uv:day3
PFCOUNT uv:total
# 内存对比
# Bitmap: 1 亿用户需要 ~12MB
# HyperLogLog: 固定 ~12KB(误差率 0.81%)
2.3 队列场景
先进先出队列 → List
# 消息队列
LPUSH queue:tasks task1
LPUSH queue:tasks task2
LPUSH queue:tasks task3
# 消费消息
RPOP queue:tasks
# 阻塞式消费(推荐)
BRPOP queue:tasks 0 # 阻塞等待
延迟队列 → ZSet
# 添加延迟任务(分数为执行时间戳)
ZADD queue:delayed 1725523200 task1
ZADD queue:delayed 1725523260 task2
# 获取到期的任务
ZRANGEBYSCORE queue:delayed 0 1725523200
# 删除已执行的任务
ZREM queue:delayed task1
发布订阅 → Pub/Sub
# 发布消息
PUBLISH channel:order:created {"order_id": 12345}
# 订阅频道
SUBSCRIBE channel:order:created
# 模式订阅
PSUBSCRIBE channel:order:*
持久化流 → Stream
# 添加消息到流
XADD stream:orders * order_id 12345 amount 99.99
# 消费消息
XREAD COUNT 10 STREAMS stream:orders 0
# 消费者组
XGROUP CREATE stream:orders group1 0
XREADGROUP GROUP group1 consumer1 COUNT 10 STREAMS stream:orders >
# 确认消息
XACK stream:orders group1 message_id
2.4 排行榜场景
简单排行榜 → ZSet
# 添加分数
ZADD leaderboard:daily 100 user1
ZADD leaderboard:daily 250 user2
ZADD leaderboard:daily 180 user3
# 获取前 10 名(从高到低)
ZREVRANGE leaderboard:daily 0 9 WITHSCORES
# 获取用户排名
ZREVRANK leaderboard:daily user1
# 获取用户分数
ZSCORE leaderboard:daily user1
# 增加分数
ZINCRBY leaderboard:daily 10 user1
多级排序 → ZSet + Hash
# 主分数(游戏得分)
ZADD leaderboard:game 10000 player1
# 附加信息(用时、等级)
HSET player:1:info time 300 level 50
# 相同分数时,按用时排序
# 需要在应用层处理
分区排行榜 → 多个 ZSet
# 按天分区
ZADD leaderboard:2025-09-05 100 user1
ZADD leaderboard:2025-09-06 150 user1
# 合并计算总分
ZUNIONSTORE leaderboard:total 7 leaderboard:2025-09-0*
2.5 社交关系场景
共同关注 → Set
# 用户关注列表
SADD user:1001:following 1002 1003 1004
SADD user:1002:following 1001 1003 1005
# 计算共同关注(交集)
SINTER user:1001:following user:1002:following
# 结果:1003
# 计算可能认识的人
SUNION user:1001:following user:1002:following
SDIFF union_result user:1001:following
好友关系 → Set
# 好友列表(双向)
SADD user:1001:friends 1002
SADD user:1002:friends 1001
# 检查是否好友
SISMEMBER user:1001:friends 1002
# 删除好友
SREM user:1001:friends 1002
SREM user:1002:friends 1001
关注粉丝 → 两个 Set
# 关注列表
SADD user:1001:following 1002 1003
# 粉丝列表
SADD user:1001:followers 1004 1005
# 检查是否关注
SISMEMBER user:1001:following 1002
# 检查是否被关注
SISMEMBER user:1001:followers 1004
2.6 地理位置场景
附近的人 → GEO
# 添加位置
GEOADD users:location 116.4074 39.9045 user1 # 北京
GEOADD users:location 121.4737 31.2304 user2 # 上海
GEOADD users:location 113.2644 23.1291 user3 # 广州
# 查找附近的人(100km 内)
GEORADIUS users:location 116.4074 39.9045 100 km
# 计算距离
GEODIST users:location user1 user2 km
# 获取位置
GEOPOS users:location user1
距离排序 → GEO + WITHDIST
# 按距离排序
GEORADIUS users:location 116.4074 39.9045 100 km WITHDIST ASC COUNT 10
2.7 状态标记场景
布尔状态 → Bitmap
# 用户签到
SETBIT user:1001:checkin 0 1 # 第 1 天
SETBIT user:1001:checkin 1 1 # 第 2 天
# 检查是否签到
GETBIT user:1001:checkin 0
# 统计签到天数
BITCOUNT user:1001:checkin
多状态 → String (位掩码)
# 权限标志位
# bit 0: 读权限,bit 1: 写权限,bit 2: 删除权限
SET user:1001:permissions 7 # 二进制 111,拥有所有权限
# 检查权限
GET user:1001:permissions
# 应用层解析:7 & 1 = 1 (有读权限)
枚举状态 → String
# 订单状态
SET order:12345:status "PENDING"
SET order:12345:status "PAID"
SET order:12345:status "SHIPPED"
# 或使用数字
SET order:12345:status 1 # 1=待支付,2=已支付,3=已发货
三、选择决策树
选择数据类型决策树:
1. 是否需要排序?
├── 是 → 2
└── 否 → 3
2. 是否需要自动排序?
├── 是 → ZSet(排行榜)
└── 否 → List(队列)
3. 是否是键值对?
├── 是 → 4
└── 否 → 5
4. 是否需要分字段操作?
├── 是 → Hash(对象)
└── 否 → String(简单缓存)
5. 是否需要去重?
├── 是 → 6
└── 否 → 7
6. 是否需要集合运算?
├── 是 → Set(交集、并集)
└── 否 → 8
7. 是否是连续数据?
├── 是 → List(列表)
└── 否 → String
8. 是否是布尔值?
├── 是 → Bitmap(签到、状态)
└── 否 → 9
9. 是否是基数统计?
├── 是 → HyperLogLog(UV 统计)
└── 否 → 10
10. 是否是地理位置?
├── 是 → GEO(附近的人)
└── 否 → Stream(消息流)
四、性能优化建议
4.1 内存优化
数据结构内存占用对比(存储 1 万元素):
String: ~1MB (最简单)
List: ~1.5MB (链表开销)
Set: ~2MB (哈希表开销)
Hash: ~1.2MB (压缩列表优化)
ZSet: ~3MB (跳表开销)
Bitmap: ~125KB (最节省)
优化技巧:
# 1. 使用 Hash 代替多个 String
# 错误:
SET user:1001:name "张三"
SET user:1001:age 25
SET user:1001:email "test@example.com"
# 正确:
HSET user:1001 name "张三" age 25 email "test@example.com"
# 2. 使用压缩列表优化
# Redis 自动优化(元素少、值小)
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# 3. 使用 Bitmap 代替 Set
# 错误:存储在线状态
SADD online_users user1 user2 user3
# 正确:
SETBIT online_users user1 1
4.2 性能优化
# 1. 批量操作
MGET key1 key2 key3 # 代替多次 GET
MSET key1 v1 key2 v2 key3 v3 # 代替多次 SET
# 2. 使用 Pipeline
# 客户端批量发送命令
# 3. 避免大 Key
# List/Set/Hash 元素控制在 5000 以内
# 4. 合理使用过期时间
# 避免同时过期(添加随机值)
五、总结
5.1 快速参考表
| 场景 | 首选类型 | 备选方案 |
|---|---|---|
| 简单缓存 | String | Hash |
| 对象存储 | Hash | String(JSON) |
| 计数器 | String(INCR) | Hash(HINCRBY) |
| 队列 | List | Stream |
| 延迟队列 | ZSet | - |
| 消息队列 | Stream | List/PubSub |
| 排行榜 | ZSet | - |
| 去重 | Set | - |
| 集合运算 | Set | - |
| UV 统计 | HyperLogLog | Bitmap |
| 签到 | Bitmap | Set |
| 地理位置 | GEO | - |
| 权限标记 | Bitmap | String |
| 社交关系 | Set | - |
5.2 核心原则
- 简单优先:能用 String 解决的不用复杂类型
- 内存效率:大数据量考虑 Bitmap、HyperLogLog
- 功能匹配:根据业务需求选择合适类型
- 性能考虑:避免大 Key、合理使用过期时间
参考资料
- Redis 官方文档 - Data Types
- Redis 内存优化
- 《Redis 设计与实现》第 3 章