Skip to content

04|hash、list、set、zset

string 只能存一个值对应一个 key。但很多数据天然是"一个对象多个属性"——用户有 name/age/city、文章有 title/author/views、商品有 name/price/stock。先用 string 方案试一下,再讲 Redis 另外四种数据结构各自解决什么问题。

一、hash:一个对象多个属性

用 string 存一个用户的三个属性:

bash
SET user:1001:name "zhangsan"
SET user:1001:age "28"
SET user:1001:city "beijing"

存是能存,但取的时候麻烦。要拿到这个用户的完整信息,得 GET 三次:

bash
GET user:1001:name
GET user:1001:age
GET user:1001:city

三个 key 分布在键空间里,改一个属性要操作一个 key,不影响其他属性这件事得靠应用层保证。key 数量也翻了几倍——一万个用户,string 方案要三万个 key。

hash 把同一个对象的所有属性收进一个 key 里:

bash
HSET user:1001 name "zhangsan" age "28" city "beijing"
HGET user:1001 name
# "zhangsan"
HGETALL user:1001
# name zhangsan
# age 28
# city beijing

HGETALL 一次命令拿回所有 field-value,不用往返三次。改 city 只动那一个 field,其他不动。

常用命令:

bash
HSET user:1001 name "zhangsan"           # 设单个 field
HGET user:1001 name                       # 取单个
HDEL user:1001 city                       # 删某个 field
HEXISTS user:1001 name                    # field 是否存在
HINCRBY user:1001 age 1                   # 某个数字 field 自增
HLEN user:1001                            # 有多少个 field

HINCRBY 在 hash 上也能原子自增——存访问量这种数字属性时,不需要取出再写回。

HGETALL 在大 hash 上慢

HGETALL 的复杂度是 O(n),n 是 field 数量。几百个 field 没问题,几千个就要用 HSCAN 分批取:

bash
HSCAN user:1001 0 COUNT 100

生产里 HGETALL 上慢日志,最常见的原因就是 field 数量超预期。

另一个更隐蔽的点:hash 在 field 少时用压缩编码(ziplist),内存很省;field 数量超过阈值(默认 512)或单个值超过 64 字节后转成 hashtable 编码,内存会跳涨。一个存了几百个短 field 的 hash 突然翻倍涨内存,多半是触发了编码转换。阈值由 hash-max-ziplist-entrieshash-max-ziplist-value 控制。

二、list:有序可重复的序列

用 string 做消息队列行不通——没有"取出一条消息同时把它从队列里删掉"的原子操作。应用层 GETDEL 是两步,中间可能插进别的客户端,同一条消息被两个消费者同时拿到。

list 是双向链表,首尾插入和删除都是 O(1),天然适合存有序序列:

bash
LPUSH queue "task1"        # 左侧(队头)插入
LPUSH queue "task2"
RPUSH queue "task3"        # 右侧(队尾)插入
LRANGE queue 0 -1         # 看全部:task2 task1 task3
LPOP queue                # 从队头取
RPOP queue                # 从队尾取
LLEN queue                # 长度

LPUSH + RPOP 组合做 FIFO 队列——先入先出。BLPOP/BRPOP 支持阻塞等待:队列为空时消费者挂起,有新元素立刻返回,应用层不用轮询空转:

bash
BLPOP queue 0             # 阻塞等队列有元素,0 表示无限等待

list 消费消息有个局限:消息 RPOP 出去就消失了——消费者处理到一半崩溃,消息就丢了。需要 ACK 机制、消费者组、消息持久化的队列场景,要用 Redis 5.0 引入的 Stream。Stream 比 list 多了消费者组、消息 ID 和 ACK 机制,专门为消息队列设计。

三、set:无序不重复的集合

用 string 存文章的标签,每个标签一个 key:

bash
SET article:1:tag1 "redis"
SET article:1:tag2 "database"

去重困难——同一个标签被重复写入,应用层得自己检查。算"两篇文章的共同标签"更麻烦,所有标签拉回应用层再算交集。

set 元素不重复,增删查都是 O(1):

bash
SADD article:1:tags "redis" "database" "nosql"
SISMEMBER article:1:tags "redis"      # 是否存在
SMEMBERS article:1:tags                # 取所有成员
SCARD article:1:tags                    # 元素个数
SREM article:1:tags "nosql"            # 删成员

真正有用的是集合运算:

bash
SINTER article:1:tags article:2:tags    # 交集:共同标签
SUNION article:1:tags article:2:tags   # 并集:所有标签
SDIFF user:1:friends user:2:friends    # 差集:user1 有但 user2 没有的好友

经典场景:社交关系的"共同好友"——SINTER user:1001:friends user:1002:friends 一条命令算出来。

但要注意数据量。SINTER/SUNION/SDIFF 的复杂度是 O(n),n 是参与运算的所有元素总数。两个集合各几十万人,这一条命令会阻塞 Redis 数秒,其他请求全排队。大数据量的集合运算应该在从库或离线环境跑,别在主库上算。

四、zset:带分数的有序集合

用 string 做排行榜,每次更新分数要走三步:

bash
GET player1_score        # 取出当前分数
# 应用层加 5、排序
SET player1_score 105

多客户端同时更新时,跟 INCR 的竞态条件一样——覆盖丢失。更麻烦的是排序,string 里存的是孤立数值,Redis 不知道谁大谁小,排一次序要遍历所有 key。

zset(sorted set)每个元素关联一个 score(浮点数),插入时自动按 score 排序:

bash
ZADD leaderboard 100 "player1" 95 "player2" 88 "player3"
ZRANGE leaderboard 0 -1 WITHSCORES         # 按 score 升序
ZREVRANGE leaderboard 0 2 WITHSCORES       # 前 3 名(降序)
ZREVRANK leaderboard "player1"             # player1 排第几(从 0 开始)
ZSCORE leaderboard "player1"               # 查 player1 的分数
ZINCRBY leaderboard 5 "player1"            # 加 5 分

插入或更新分数是 O(log n),查排名也快——千万级用户的排行榜也能响应。

zset 的其他用途:

带权重的任务队列——score 存优先级,按优先级取任务。

延迟队列——score 存"应该被处理的时间戳",消费者用 ZRANGEBYSCORE 取出到期的任务:

bash
ZRANGEBYSCORE delay_queue 0 "$(date +%s)" LIMIT 0 10

处理完 ZREM 删除。但延迟队列同样没 ACK——消费者崩溃任务就丢。生产延迟队列用专门的消息中间件更稳妥。

五、五种类型怎么选

要存什么用什么为什么
单值缓存、计数器string单值场景最直接
一个对象的多个属性hash一个 key 装多个 field,省 key 数、查对象一次往返
消息序列、最近 N 条list有序、可重复、首尾操作快
去重、标签、共同关系set不重复、集合运算
排行榜、按分数取zset自动排序、按区间取

选错能跑通但性能差几个数量级。用 string 存排行榜:每次更新要 GET 取值、应用层排序、再 SET 回去——多往返、非原子、慢。用 zset 存同一批数据,一条 ZADDZINCRBY 就够了。

六、其他类型

除了这五种,Redis 后续版本加了一些专门类型:

  • Stream:带消费者组、消息 ID、ACK 机制的消息队列,比 list 当队列可靠
  • Geospatial:地理位置索引,算距离、范围查询
  • HyperLogLog:基数估算,UV 这种"去重计数"用很少内存
  • Bitmap:位操作,存布尔状态、做位统计

这些用得没有五大类型多,用到时查文档,本系列主线讲五大类型。