2018-06-08
很多人刚开始使用 Redis 时,只把它当作简单的 `key-value` 缓存。实际上,Redis 真正强大的地方在于它提供了多种数据结构。不同的数据结构适合不同的业务场景,选对结构往往比单纯记住命令更重要。
Redis 是一个高性能的内存数据库,常被用作缓存、计数器、排行榜、消息队列、分布式锁和会话存储。
很多人刚开始使用 Redis 时,只把它当作简单的 key-value 缓存。实际上,Redis 真正强大的地方在于它提供了多种数据结构。不同的数据结构适合不同的业务场景,选对结构往往比单纯记住命令更重要。
本文主要围绕 Redis 中几种常用数据结构展开:
String 是 Redis 中最基础、最常用的数据结构。它可以存储字符串、数字、JSON 字符串、序列化后的对象等。
SET user:1:name "Tom"
GET user:1:name
SET count 1
INCR count
DECR count
SETEX token:abc 3600 "user-1"
TTL token:abc
可以把一个对象序列化成 JSON 后存入 Redis:
SET user:1 '{"id":1,"name":"Tom","age":20}'
GET user:1
这种方式简单直接,适合读取整个对象的场景。
Redis 的 INCR 和 DECR 是原子操作,非常适合做计数器:
INCR article:1001:views
INCR video:2001:likes
常见用法包括:
登录 token、验证码、短信码等通常都有过期时间,可以使用 SETEX:
SETEX login:token:abc123 7200 "user-1"
SETEX sms:code:13800000000 300 "9527"
这样 Redis 会自动删除过期数据,不需要业务代码额外清理。
Hash 可以理解为一个 key 对应多个 field-value。它很适合存储对象,尤其是对象字段需要单独读写的场景。
HSET user:1 name "Tom" age 20 city "Shanghai"
HGET user:1 name
HMGET user:1 name age
HGETALL user:1
HINCRBY user:1 age 1
HDEL user:1 city
HSET user:1 id 1 name "Tom" age 20 level 3
HGET user:1 name
HINCRBY user:1 level 1
如果只需要修改用户的某一个字段,Hash 比整个 JSON 字符串更方便。
HSET product:1001 stock 500 price 99 sales 0
HINCRBY product:1001 stock -1
HINCRBY product:1001 sales 1
库存、销量、价格等字段可以独立更新。
如果业务经常一次性读取整个对象,可以使用 String 存 JSON。
如果业务经常修改或读取对象中的某几个字段,Hash 更合适。
例如:
List 是一个按照插入顺序排列的列表,可以从两端插入或弹出元素。
LPUSH queue:email "job-1"
LPUSH queue:email "job-2"
RPOP queue:email
RPUSH messages "msg-1"
RPUSH messages "msg-2"
LRANGE messages 0 -1
生产者写入任务:
LPUSH queue:order "order-1001"
LPUSH queue:order "order-1002"
消费者取出任务:
RPOP queue:order
也可以使用阻塞式命令:
BRPOP queue:order 10
BRPOP 在队列为空时会等待,适合消费者进程轮询任务。
LPUSH article:comments:1001 "comment-1"
LPUSH article:comments:1001 "comment-2"
LRANGE article:comments:1001 0 9
这种方式适合取最新评论、最新动态等。
List 适合简单队列,但如果业务要求更强的消息确认、失败重试、消费组等能力,应该优先考虑 Redis Stream,或者使用专业消息队列。
Set 用来存储不重复的元素,适合去重、标签、关注关系、集合运算等场景。
SADD user:1:tags "python" "redis" "mysql"
SMEMBERS user:1:tags
SISMEMBER user:1:tags "redis"
SREM user:1:tags "mysql"
SCARD user:1:tags
SADD article:1001:tags "redis" "database" "cache"
SADD article:1002:tags "redis" "python"
Set 天然去重,适合保存标签。
SADD user:1:following 2 3 4
SADD user:2:followers 1
判断是否关注:
SISMEMBER user:1:following 2
SINTER user:1:following user:2:following
集合运算是 Set 的重要能力:
SINTER:交集SUNION:并集SDIFF:差集例如社交系统中的共同好友、共同关注、兴趣匹配,都可以用 Set 来实现。
Sorted Set,也叫 ZSet。它和 Set 一样元素不重复,但每个元素都会关联一个分数 score,Redis 会根据分数排序。
ZADD rank:game 100 "Tom"
ZADD rank:game 200 "Jerry"
ZADD rank:game 150 "Alice"
ZRANGE rank:game 0 -1 WITHSCORES
ZREVRANGE rank:game 0 9 WITHSCORES
ZINCRBY rank:game 50 "Tom"
ZRANK rank:game "Tom"
ZREVRANK rank:game "Tom"
ZADD rank:article 100 "article-1"
ZADD rank:article 300 "article-2"
ZADD rank:article 200 "article-3"
获取前 10 名:
ZREVRANGE rank:article 0 9 WITHSCORES
增加文章热度:
ZINCRBY rank:article 1 "article-1"
可以把时间戳作为 score:
ZADD delay:queue 1730000000 "task-1"
ZADD delay:queue 1730000060 "task-2"
消费者查询当前时间之前到期的任务:
ZRANGEBYSCORE delay:queue 0 1730000000
这种方式适合实现轻量级延迟任务。
把发布时间作为 score:
ZADD user:1:feed 1730000000 "post-1001"
ZADD user:1:feed 1730000100 "post-1002"
按时间倒序取最新内容:
ZREVRANGE user:1:feed 0 19
Bitmap 本质上是基于 String 的位操作。它适合存储大量只有两种状态的数据,例如是否签到、是否在线、是否活跃。
SETBIT sign:2026-04 0 1
GETBIT sign:2026-04 0
BITCOUNT sign:2026-04
假设一个月最多 31 天,可以用每一位表示某一天是否签到:
SETBIT user:1:sign:2026-04 0 1
SETBIT user:1:sign:2026-04 1 1
SETBIT user:1:sign:2026-04 2 0
统计本月签到天数:
BITCOUNT user:1:sign:2026-04
如果用用户 ID 作为偏移量,可以记录某天哪些用户活跃:
SETBIT active:2026-04-24 10001 1
GETBIT active:2026-04-24 10001
统计当天活跃用户数:
BITCOUNT active:2026-04-24
Bitmap 的优势是非常节省空间。对于大量布尔值场景,比用 Set 存用户 ID 更省内存。
HyperLogLog 用于统计不重复元素数量,也就是基数统计。它的特点是占用空间极小,但结果有一定误差。
PFADD uv:article:1001 user-1 user-2 user-3
PFADD uv:article:1001 user-2
PFCOUNT uv:article:1001
PFADD uv:site:2026-04-24 user-1
PFADD uv:site:2026-04-24 user-2
PFADD uv:site:2026-04-24 user-1
PFCOUNT uv:site:2026-04-24
即使同一个用户访问多次,也只会统计一次。
HyperLogLog 适合只关心数量、不关心具体成员的场景。
如果你需要知道“哪些用户访问过”,应该使用 Set。
如果你只需要知道“大概有多少独立用户访问过”,HyperLogLog 更节省空间。
Stream 是 Redis 5.0 引入的数据结构,适合实现消息队列、事件流、日志流等。
相比 List,Stream 支持消息 ID、消费组、消费确认和消息追踪。
XADD order:stream * orderId 1001 status created
XADD order:stream * orderId 1002 status paid
XRANGE order:stream - +
XREAD COUNT 2 STREAMS order:stream 0
创建消费组:
XGROUP CREATE order:stream group-1 0 MKSTREAM
消费者读取消息:
XREADGROUP GROUP group-1 consumer-1 COUNT 1 STREAMS order:stream >
确认消息:
XACK order:stream group-1 1730000000000-0
XADD order:events * orderId 1001 event created
XADD order:events * orderId 1001 event paid
XADD order:events * orderId 1001 event shipped
不同消费者可以处理不同逻辑,例如:
Stream 比 List 更适合消息队列,因为它支持:
如果项目已经使用 Redis,且消息量不算特别大,Stream 是一个实用选择。
下面是一个简单对照表:
| 数据结构 | 适合场景 |
|---|---|
| String | 缓存值、计数器、验证码、Token |
| Hash | 用户对象、商品对象、字段独立更新 |
| List | 简单队列、最新列表、评论列表 |
| Set | 去重、标签、关注关系、集合运算 |
| Sorted Set | 排行榜、延迟队列、按分数排序 |
| Bitmap | 签到、活跃状态、布尔标记 |
| HyperLogLog | UV、去重数量统计 |
| Stream | 消息队列、事件流、消费组 |
Redis 的 key 命名建议保持清晰和统一:
业务名:对象名:对象ID:字段
例如:
user:1
user:1:profile
user:1:following
article:1001:views
rank:article:daily
uv:site:2026-04-24
几个建议:
: 分隔层级KEYS * 扫描全量 key,应该使用 SCANRedis 不是只能做缓存。它的多种数据结构可以覆盖很多常见业务需求。
简单来说:
真正用好 Redis 的关键,不是记住所有命令,而是根据业务场景选择合适的数据结构。数据结构选对了,代码会更简单,性能也会更稳定。