Skip to content
清晨的一缕阳光
返回

Redis 数据类型选择指南

Redis 数据类型选择指南

Redis 提供了多种数据类型,如何选择合适的数据类型是设计高效 Redis 应用的关键。本文将通过实际场景,帮助你选择最佳的数据类型。

一、数据类型总览

1.1 基础数据类型

Redis 数据类型体系:
┌─────────────────────────────────────┐
│ 基础类型                            │
│ ├── String(字符串)                │
│ ├── List(列表)                    │
│ ├── Set(集合)                     │
│ ├── Hash(哈希)                    │
│ └── ZSet(有序集合)                │
├─────────────────────────────────────┤
│ 高级类型                            │
│ ├── Bitmap(位图)                  │
│ ├── HyperLogLog(基数统计)         │
│ ├── GEO(地理位置)                 │
│ └── Stream(流)                    │
└─────────────────────────────────────┘

1.2 性能对比

类型时间复杂度内存效率适用场景
StringO(1)缓存、计数器
ListO(1)队列、栈
SetO(1)去重、集合运算
HashO(1)对象存储
ZSetO(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 快速参考表

场景首选类型备选方案
简单缓存StringHash
对象存储HashString(JSON)
计数器String(INCR)Hash(HINCRBY)
队列ListStream
延迟队列ZSet-
消息队列StreamList/PubSub
排行榜ZSet-
去重Set-
集合运算Set-
UV 统计HyperLogLogBitmap
签到BitmapSet
地理位置GEO-
权限标记BitmapString
社交关系Set-

5.2 核心原则

  1. 简单优先:能用 String 解决的不用复杂类型
  2. 内存效率:大数据量考虑 Bitmap、HyperLogLog
  3. 功能匹配:根据业务需求选择合适类型
  4. 性能考虑:避免大 Key、合理使用过期时间

参考资料


分享这篇文章到:

上一篇文章
RAG 重排序与后处理实战
下一篇文章
RAG 向量检索工程实践