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 做轻量级消息队列的核心机制。
RPOPLPUSH 和 BRPOPLPUSH 是"原子地从一个队列弹到另一个队列",用来实现可靠消费:任务从 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 30Redis 当消息队列是中小项目最常用的方案。比起 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" # 检查是否在黑名单 -> 1UV 统计提一句:用户量到百万级以上,用 set 存所有用户 ID 会很占内存(每个 ID 几十字节)。这种场景改用 HyperLogLog(PFADD / PFCOUNT),12KB 内存就能估算亿级 UV,误差仅 0.81%,非常适合"日活、月活"这种不需要精确的统计。
小结
list(有序可重复)和 set(无序不重复)各有所长:list 的核心是头尾 O(1) + 阻塞弹出,适合队列和最新列表;set 的核心是O(1) 判断存在 + 集合运算,适合去重和社交场景。下一篇讲 Redis 的明星类型——有序集合 zset,做排行榜的最佳选择。
← 上一篇 Redis 哈希命令详解
下一篇 Redis 有序集合命令 →