Redis 列表与集合命令

本篇讲两个相关的类型:list(列表)set(集合)。它们都存"多个元素",但一个有序可重复、一个无序不重复,适合截然不同的场景。放在一起看更容易理解它们的差异。

一、列表(list)

1. list 基础:LPUSH / RPUSH / LRANGE

# list:有序、可重复,头尾操作 O(1)
LPUSH letters "a" "b" "c"   # 头插(左),结果是 [c, b, a]
RPUSH letters "x" "y"       # 尾插(右),结果是 [c, b, a, x, y]

LRANGE letters 0 -1        # 取全部(下标 0 到末尾)
# 1) "c"
# 2) "b"
# 3) "a"
# 4) "x"
# 5) "y"

LRANGE letters 0 2         # 取下标 0~2(前三个)
LRANGE letters -3 -1       # 取最后三个

LLEN letters               # 长度:5

LINDEX letters 0           # 取下标 0(第一个,即 c)
LINDEX letters -1          # 取最后一个(即 y)

list 在 Redis 里是双向链表(早期)或 quicklist(3.2 之后,链表节点里嵌 ziplist)实现。无论哪种,头尾插入弹出都是 O(1),但按下标访问中间元素是 O(N)。所以 list 适合"头尾操作"的场景,不适合频繁随机访问。

LRANGE key start stop 的下标支持负数(从尾部数):-1 是最后一个,-2 是倒数第二个。取全部用 0 -1

2. 弹出与阻塞:POP / BLPOP

# 弹出(取出并删除)
LPOP letters               # 从头弹出 -> "c",剩 [b, a, x, y]
RPOP letters               # 从尾弹出 -> "y",剩 [b, a, x]
LPOP letters 2             # 一次弹多个(Redis 6.2+),弹出 b 和 a

# 阻塞弹出:队列空时等待,常做任务队列
BLPOP task_queue 0         # 0 表示无限等待,直到有元素
BLPOP task_queue 30        # 最多等 30 秒,超时返回 nil
BRPOP task_queue 30        # 从右阻塞弹出

# 弹出并推入另一个队列(原子,跨队列搬运)
RPOPLPUSH source_queue dest_queue
BRPOPLPUSH source_queue dest_queue 30   # 阻塞版

# 修改元素(LSET 需指定下标)
LSET letters 0 "new_value"

BLPOP / BRPOP 是消息队列的关键。如果用 RPOP 消费,队列空时消费者要不停轮询,既浪费 CPU 又增加延迟。而 BRPOP queue 0 会让消费者"睡着等",直到生产者推入新元素立即被唤醒——几乎零延迟、零空转。这是用 Redis 做轻量级消息队列的核心机制

RPOPLPUSHBRPOPLPUSH 是"原子地从一个队列弹到另一个队列",用来实现可靠消费:任务从 source 弹到 processing 后才被处理,处理完再从 processing 删除;如果消费者崩溃,任务还在 processing 里,可以由其他消费者重新处理。

3. LTRIM / LREM / LINSERT

# LTRIM:保留指定下标范围,其余删除
# 用途:做"最新 N 条"列表(超出长度的旧数据自动丢弃)
RPUSH latest_news "news1"
RPUSH latest_news "news2"
RPUSH latest_news "news3"
LTRIM latest_news 0 99     # 只保留前 100 条

# 经典模式:LPUSH + LTRIM = 容量固定的"最新列表"
# 每次新增一条:
LPUSH latest_news "news_new"
LTRIM latest_news 0 99
# 结果:永远是最近 100 条新闻,旧的自然而然被挤掉

# LREM:删除指定值的元素
LREM list 2 "x"            # 从头开始删 2 个值为 "x" 的元素
LREM list -2 "x"           # 从尾开始删 2 个
LREM list 0 "x"            # 删除所有值为 "x" 的

# LINSERT:在某个值前/后插入
LINSERT list BEFORE "anchor" "new"
LINSERT list AFTER "anchor" "new"

LTRIM 配合 LPUSH 是"最新 N 条"列表的经典套路——博客最新文章、用户最近浏览记录、聊天最近消息。每次新增后 LTRIM 0 N-1 截断,旧数据自动丢弃,列表长度永远不超过 N。比手动 LLEN 检查 + RPOP 简单得多。

4. list 实战:消息队列

# 经典用法 1:简单 FIFO 队列(先进先出)
# 生产者:LPUSH(头插)
LPUSH email_queue '{"to":"a@b.com","subject":"hi"}'
LPUSH email_queue '{"to":"c@d.com","subject":"hello"}'

# 消费者:BRPOP(尾弹,阻塞版)
BRPOP email_queue 30
# 1) "email_queue"
# 2) "{\"to\":\"a@b.com\",\"subject\":\"hi\"}"   # 先进先出

# 经典用法 2:延迟队列(配合 zset,见 sorted-set 篇)

# 经典用法 3:安全队列(RPOPLPUS 保证消费可靠性)
# 消费时把任务从 source 移到 processing,
# 处理完再从 processing 删;崩溃了还能从 processing 找回
BRPOPLPUSH email_queue processing_queue 30

Redis 当消息队列是中小项目最常用的方案。比起 RabbitMQ、Kafka,它无需额外运维,部署一个 Redis 就够了。优点:简单、低延迟、单机十万级吞吐。缺点:消息可靠性不如专业 MQ(虽然 BRPOPLPUSH 能保证消费可靠,但持久化、消费组、回溯还得用 Stream),大数据量场景仍建议 Kafka。

二、集合(set)

5. set 基础:SADD / SMEMBERS / SISMEMBER

# set:无序、不重复
SADD tags "js" "css" "html"
SADD tags "js"             # 重复添加,实际还是只有一份

SMEMBERS tags              # 所有成员(顺序无序)
# 1) "js"
# 2) "css"
# 3) "html"

SISMEMBER tags "js"        # 是否存在 -> 1
SISMEMBER tags "rust"      # 不存在 -> 0

SCARD tags                 # 成员数:3

SREM tags "html"           # 删除成员
SMEMBERS tags              # 现在只剩 js, css

# 随机取成员
SRANDMEMBER tags 2         # 随机取 2 个(可能重复)
SRANDMEMBER tags -2        # 随机取 2 个(允许重复)
SPOP tags 2                # 随机弹出(取出并删除)

# 把成员从一个 set 移到另一个
SMOVE tags favorites "js"

set 的判断成员是否存在(SISMEMBER)是 O(1)——这是它相比 list 的最大优势(列表里查成员要 O(N))。所以"判断某用户是否已签到""某 IP 是否在黑名单"这类查询,用 set 远比用 list 高效。

6. set 的杀手锏:集合运算

set 真正的强项是原生支持交集、并集、差集运算。这是关系数据库做起来很费力的事,Redis 一条命令搞定:

# 集合运算:set 的杀手锏
SADD user:1:follow "alice" "bob" "carol"     # 我关注的
SADD user:2:follow "bob" "carol" "dave"      # 你关注的

# 交集:我和你共同关注了谁
SINTER user:1:follow user:2:follow
# 1) "bob"
# 2) "carol"

# 并集:我和你关注的所有人(去重)
SUNION user:1:follow user:2:follow
# 1) "alice"
# 2) "bob"
# 3) "carol"
# 4) "dave"

# 差集:我关注但你没关注的
SDIFF user:1:follow user:2:follow
# 1) "alice"

# 把运算结果存到新 key(避免反复算)
SINTERSTORE common user:1:follow user:2:follow
SUNIONSTORE all user:1:follow user:2:follow

"共同好友""共同关注""推荐你可能认识的人"这类社交场景,在 MySQL 里要写复杂 JOIN,数据量大时还很慢;而在 Redis 里用 SINTER 几毫秒就能算出来。这就是为什么社交应用特别偏爱 Redis set

注意集合运算的复杂度:小集合没问题,但大集合做交集会很慢(O(N),N 是所有集合的总大小)。生产环境用大集合运算前,先确认数据规模,或用 SINTERSTORE 把结果缓存起来。

7. set 典型应用

# 用法 1:标签系统
SADD post:1:tags "go" "redis"
SADD post:2:tags "redis" "mysql"
# 找"同时有 go 和 redis 标签的文章" -> 用集合运算

# 用法 2:今日 UV / 活跃用户(每个用户只计一次)
SADD active:20240801 "user1" "user2" "user3"
SCARD active:20240801            # 今日活跃用户数
# 注:用户量大时改用 HyperLogLog,12KB 就能估算亿级 UV

# 用法 3:抽奖
SADD lottery "u1" "u2" "u3" "u4" "u5"
SPOP lottery 1                   # 抽 1 个中奖者(抽出后移出)
SRANDMEMBER lottery 3            # 抽 3 个(不移出,可重复抽同一人)

# 用法 4:黑白名单
SADD blacklist:ip "1.2.3.4" "5.6.7.8"
SISMEMBER blacklist:ip "1.2.3.4"  # 检查是否在黑名单 -> 1

UV 统计提一句:用户量到百万级以上,用 set 存所有用户 ID 会很占内存(每个 ID 几十字节)。这种场景改用 HyperLogLog(PFADD / PFCOUNT),12KB 内存就能估算亿级 UV,误差仅 0.81%,非常适合"日活、月活"这种不需要精确的统计。

小结

list(有序可重复)和 set(无序不重复)各有所长:list 的核心是头尾 O(1) + 阻塞弹出,适合队列和最新列表;set 的核心是O(1) 判断存在 + 集合运算,适合去重和社交场景。下一篇讲 Redis 的明星类型——有序集合 zset,做排行榜的最佳选择。

← 上一篇 Redis 哈希命令详解

下一篇 Redis 有序集合命令

✈️💬