558 lines
26 KiB
Markdown
558 lines
26 KiB
Markdown
---
|
||
tags: [Redis, 缓存, 命令, Stream, PubSub, Lua]
|
||
create time: 2026-05-15 18:12
|
||
---
|
||
|
||
# Redis 基本命令速查
|
||
|
||
## 概述
|
||
|
||
本文按数据类型分类梳理 Redis 常用命令,标注**时间复杂度**、典型场景和生产避坑点。覆盖 String / Hash / List / Set / ZSet 五大基础类型,以及 Pub/Sub、Stream、Geo、Lua 脚本等进阶能力。每条命令配有 Go(go-redis)代码示例,方便直接复制使用。
|
||
|
||
> [!NOTE] 怎么用这份速查表?
|
||
> - 日常开发直接 Ctrl+F 搜命令名
|
||
> - 遇到新业务需求先看数据类型选型流程图,再查对应章节
|
||
> - 每个章节末尾的 callout 是"老手踩过的坑",建议通读一遍
|
||
|
||
## 数据类型选型速查
|
||
|
||
遇到业务需求时,按以下流程选择合适的 Redis 数据结构:
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
start["需要存什么?"] --> q1{"单值 / 键值对?"}
|
||
q1 -- 是 --> s1["String<br/>缓存, 计数器, 分布式锁"]
|
||
q1 -- 否 --> q2{"多个字段 / 对象?"}
|
||
q2 -- 是 --> s2["Hash<br/>用户信息, 配置, 表单数据"]
|
||
q2 -- 否 --> q3{"需要排序 / 排名?"}
|
||
q3 -- 是 --> s3["ZSet<br/>排行榜, 延迟队列, 范围查询"]
|
||
q3 -- 否 --> q4{"需要去重 / 集合运算?"}
|
||
q4 -- 是 --> s4["Set<br/>标签, 共同关注, UV 统计"]
|
||
q4 -- 否 --> q5{"需要队列 / 栈?"}
|
||
q5 -- 是 --> s5["List<br/>消息队列, 最新列表, 栈"]
|
||
q5 -- 否 --> s6["Pub/Sub 或 Stream<br/>广播通知 / 可靠消息队列"]
|
||
```
|
||
|
||
> [!TIP] Stream 也是数据类型
|
||
> Redis 5.0 新增的 Stream 适合**需要持久化和消费确认**的消息场景。选型时如果 Pub/Sub 的 "fire-and-forget" 不满足需求,直接上 Stream(详见下方 Stream 章节)。
|
||
|
||
## String — 字符串操作
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `SET key value [EX s] [PX ms] [NX\|XX]` | OK / nil | O(1) | **万能设置**,EX/PX 设过期,NX 仅不存在时写,XX 仅存在时写 |
|
||
| `GET key` | value / nil | O(1) | 获取值 |
|
||
| `MGET key [key...]` | values array | O(N) | 批量获取 |
|
||
| `SETRANGE key offset value` | length | O(1)* | 覆盖指定偏移量字节(*近似) |
|
||
| `APPEND key value` | new-length | O(1)* | 追加到末尾 |
|
||
| `INCR key` | incremented val | O(1) | 原子 +1 |
|
||
| `INCRBY key increment` | incremented val | O(1) | 原子 +N |
|
||
| `DECRBY key decrement` | decremented val | O(1) | 原子 -N |
|
||
| `STRLEN key` | length | O(1) | 获取字符串长度 |
|
||
| `GETSET key value` | old value | O(1) | **已废弃**,用 `SET key value NX` 替代 |
|
||
| `GETRANGE key start end` | substring | O(N) | 截取子串(end 为 -1 表示末尾) |
|
||
| `SETEX key seconds value` | OK | O(1) | **已废弃**,用 `SET key value EX seconds` 替代 |
|
||
| `PSETEX key milliseconds value` | OK | O(1) | **已废弃**,用 `SET key value PX milliseconds` 替代 |
|
||
| `MSET key value [key value ...]` | OK | O(N) | 原子设置多个键 |
|
||
|
||
```go
|
||
// SET 条件写入——分布式锁的核心
|
||
rdb.Set(ctx, "lock:order:"+orderId, "1", 3*time.Second, redis.KeepTTL|redis.OnlyIfAbsent)
|
||
|
||
// INCR 做限流计数器
|
||
rdb.IncrBy(ctx, "rate:user:"+userID, 1)
|
||
```
|
||
|
||
> [!WARNING] MGET ≠ 事务保证
|
||
> MGET 是并发执行 N 次 GET,不保证原子性。需要原子读多个字段请用 `MULTI` 或 Lua 脚本。
|
||
|
||
## Hash — 哈希操作
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `HSET key field value [field value ...]` | added / changed count | O(N) | 设置一个或多个字段 |
|
||
| `HGET key field` | value / nil | O(1) | 获取单个字段 |
|
||
| `HMGET key field [field ...]` | values array | O(N) | 批量获取多个字段 |
|
||
| `HGETALL key` | all fields | O(N) | 获取全部键值对 ⚠️ 大 hash 慎用 |
|
||
| `HDEL key field [field ...]` | deleted count | O(N) | 删除字段 |
|
||
| `HEXISTS key field` | 1 or 0 | O(1) | 判断字段是否存在 |
|
||
| `HINCRBY key field increment` | new value | O(1) | 原子递增字段值 |
|
||
| `HSCAN key cursor MATCH pattern COUNT n` | cursor, entries | O(N) | 安全遍历,不用 HKEYS/HVALS 全量扫 |
|
||
|
||
```go
|
||
// 用户信息缓存——部分更新
|
||
pipe := rdb.Pipeline()
|
||
pipe.HSet(ctx, "user:42", "email", "new@email.com")
|
||
pipe.HSet(ctx, "user:42", "phone", "1380000")
|
||
pipe.Exec(ctx) // 管道内原子执行
|
||
```
|
||
|
||
> [!TIP] HGETALL vs HSCAN
|
||
> `HGETALL` 一步到位获取全部字段,但大 hash(>10K 字段)会阻塞主线程。生产环境推荐 `HSCAN` 分片读取。
|
||
|
||
## List — 列表操作
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `LPUSH key elem [elem ...]` | list length | O(N) | 从左侧推入 |
|
||
| `RPUSH key elem [elem ...]` | list length | O(N) | 从右侧推入 |
|
||
| `LPOP key` | element / nil | O(1) | 弹出左侧元素 |
|
||
| `RPOP key` | element / nil | O(1) | 弹出右侧元素 |
|
||
| `LRANGE key start stop` | subarray | O(S+N) | 范围查询 S=start, N=stop-start |
|
||
| `LLEN key` | length | O(1) | 列表长度 |
|
||
| `LINDEX key index` | element | O(N) | 按索引获取 |
|
||
| `LTRIM key start stop` | OK | O(N) | 只保留指定范围(固定队列长度必备) |
|
||
| `BLPOP key [key ...] timeout` | element / nil | O(timeout) | **阻塞式** LPOP |
|
||
| `BRPOP key [key ...] timeout` | element / nil | O(timeout) | **阻塞式** RPOP |
|
||
| `RPOPLPUSH src dst` | element | O(1) | **已废弃**,用 LMOVE 替代 |
|
||
| `LMOVE key where1 where2` | element | O(1) | 弹出一端、push 到另一端(支持 LEFT/RIGHT) |
|
||
|
||
```go
|
||
// BRPOP 消费者模型
|
||
val, err := rdb.BRPop(ctx, 5*time.Second, "task_queue").Result()
|
||
if err == redis.Nil {
|
||
return nil // 超时,无消息
|
||
}
|
||
```
|
||
|
||
> [!QUESTION] LPUSH + RPOP 还是 RPUSH + LPOP?
|
||
> 两者性能相同(都是两端操作)。选择取决于语义——"先到的先处理"用 LPUSH+RPOP(FIFO),反之则相反。
|
||
|
||
## Set — 集合操作
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `SADD key member [member ...]` | added count | O(N) | 添加成员 |
|
||
| `SREM key member [member ...]` | removed count | O(N) | 移除成员 |
|
||
| `SMEMBERS key` | all members | O(N) | 获取全部 ⚠️ |
|
||
| `SISMEMBER key member` | 1 or 0 | O(1) | 判断成员 |
|
||
| `SCARD key` | cardinality | O(1) | 元素个数 |
|
||
| `SRANDMEMBER key [count]` | member(s) | O(N) | 随机取(不删除) |
|
||
| `SPOP key [count]` | member(s) | O(N) | 随机取并删除 |
|
||
| `SINTER key [key ...]` | intersection | O(N*M) | 交集 ⚠️ 跨节点成本高 |
|
||
| `SUNION key [key ...]` | union | O(N*M) | 并集 |
|
||
| `SDIFF key [key ...]` | difference | O(N*M) | 差集 |
|
||
| `SINTERSTORE dst key [key ...]` | result count | O(N*M) | 交集结果存入新 key |
|
||
|
||
```go
|
||
// 共同关注——用户 A 和用户 B 都关注的人
|
||
common := rdb.SInter(ctx, "follow:userA", "follow:userB").Val()
|
||
|
||
// 抽奖系统——随机取 N 个不重复中奖者
|
||
winners := rdb.SPopN(ctx, "entry:lottery", 5).Val()
|
||
```
|
||
|
||
> [!TIP] Set 的典型应用场景
|
||
> - **去重**:UV 统计(`SADD` + `SCARD`)
|
||
> - **交集/并集**:共同好友、标签聚合
|
||
> - **随机抽样**:`SPOP` / `SRANDMEMBER` 做抽奖或推荐
|
||
|
||
## ZSet —— 有序集合
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `ZADD key score member [score member ...]` | added/changed | O(N log N) | 添加/更新 |
|
||
| `ZSCORE key member` | score / nil | O(1) | 获取分数 |
|
||
| `ZINCRBY key increment member` | new score | O(log N) | 增加分数 |
|
||
| `ZRANGE key start stop [WITHSCORES]` | members | O(log N+M) | 正序范围 |
|
||
| `ZREVRANGE key start stop [WITHSCORES]` | members | O(log N+M) | 倒序范围 |
|
||
| `ZRANGEBYSCORE key min max [WITHSCORES]` | members | O(log N+M) | 分数区间 |
|
||
| `ZRANK key member` | rank / nil | O(log N) | 升序排名 |
|
||
| `ZREVRANK key member` | reverse rank / nil | O(log N) | 降序排名 |
|
||
| `ZCARD key` | count | O(1) | 元素数 |
|
||
| `ZCOUNT key min max` | count | O(log N) | 分数范围内元素数 |
|
||
| `ZREM key member [member ...]` | removed | O(M log N) | 移除 |
|
||
| `ZREMRANGEBYRANK key start stop` | removed | O(log N+M) | 按排名删 |
|
||
| `ZREMRANGEBYSCORE key min max` | removed | O(log N+M) | 按分数删 |
|
||
| `ZMSCORE key member [member ...]` | scores | O(N) | 批量获取分数(7.0+) |
|
||
| `ZPOPMAX key [count]` | top members | O(log N+M) | 弹出最高分 |
|
||
| `ZPOPMIN key [count]` | bottom members | O(log N+M) | 弹出最低分 |
|
||
| `ZINTERSTORE dst numkeys key [numkeys...]` | cardinality | O(N×K+K log K) | 多 ZSet 求交集存新 key |
|
||
| `ZUNIONSTORE dst numkeys key [numkeys...]` | cardinality | O(N×K+K log K) | 多 ZSet 求并集存新 key |
|
||
| `ZRANDMEMBER key [count] [WITHSCORES]` | members | O(N) | 随机取(7.0+) |
|
||
| `ZRANGEBYLEX key min max [LIMIT offset count]` | members | O(log N+M) | 同分数按 member 字典序排 |
|
||
| `ZSCAN key cursor MATCH pattern COUNT n` | cursor, entries | O(N) | 安全遍历 |
|
||
|
||
```go
|
||
// 排行榜——获取 TOP 10
|
||
top10 := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Val()
|
||
for _, z := range top10 {
|
||
fmt.Printf("%s: %.1f\n", z.Member, z.Score)
|
||
}
|
||
|
||
// 分数区间查询——80~100 分的学生
|
||
students := rdb.ZRangeByScore(ctx, "score:classA", redis.ZRangeBy{
|
||
Min: "80", Max: "100",
|
||
}).Val()
|
||
```
|
||
|
||
> [!TIP] 分数边界写法
|
||
> `[` 包含、`(` 不包含。例如 `"[80"` 表示 ≥80,`"(100"` 表示 <100。`-inf` 和 `+inf` 表示无穷大。
|
||
|
||
## 通用命令
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `DEL key [key ...]` | deleted count | O(N) | N = key 的平均大小,大 key 会阻塞 |
|
||
| `EXPIRE key seconds` | 1 or 0 | O(1) | 设过期时间(秒) |
|
||
| `PEXPIRE key milliseconds` | 1 or 0 | O(1) | 毫秒级过期 |
|
||
| `TTL key` | seconds / -1 / -2 | O(1) | 剩余存活秒数 |
|
||
| `PTTL key` | milliseconds | O(1) | 剩余存活毫秒 |
|
||
| `EXPIREAT key timestamp` | 1 or 0 | O(1) | 按 Unix 时间戳过期 |
|
||
| `KEYS pattern` | matching keys | **O(N)** | ⛔ 全库扫描,生产禁用! |
|
||
| `SCAN cursor MATCH pattern COUNT n` | cursor, keys | **增量迭代 O(1)** | ✅ 安全的模糊查找 |
|
||
| `DBSIZE` | count | O(1) | 当前数据库 key 总数 |
|
||
| `SELECT index` | OK | O(1) | 切换数据库(0~15) |
|
||
| `FLUSHDB` | OK | O(N) | 清空当前库 |
|
||
| `FLUSHALL` | OK | O(N) | 清空所有库 |
|
||
| `TYPE key` | string/hash/list... | O(1) | 查看数据类型 |
|
||
| `EXISTS key [key ...]` | existing count | O(N) | 判断 key 是否存在,支持批量 |
|
||
| `OBJECT ENCODING key` | encoding name | O(1) | 查看底层编码(排查性能利器) |
|
||
| `UNLINK key [key ...]` | deleted count | O(N) | 异步删除,不阻塞主线程 ✅ |
|
||
| `MOVE key db` | 1 or 0 | O(1) | 移动到其他数据库(集群不支持) |
|
||
| `RENAME key newkey` | OK | O(1) | 重命名(不管类型) |
|
||
| `RANDOMKEY` | key / nil | O(1) | 随机返回一个 key(集群不支持) |
|
||
|
||
```go
|
||
// 优雅删除大 key——首选 UNLINK(异步删除,不阻塞主线程)
|
||
rdb.Unlink(ctx, "large:key1", "large:key2")
|
||
|
||
// 或者用 SSCAN/HSCAN 分批删除字段
|
||
iter := rdb.HScan(ctx, "big:hash", 0, "", 100).Iterator()
|
||
for iter.Next(ctx) {
|
||
rdb.HDel(ctx, "big:hash", iter.Val()...)
|
||
}
|
||
```
|
||
|
||
> [!TIP] DEL vs UNLINK
|
||
> `DEL` 是同步删除,大 key(如百万元素的 Set)会阻塞主线程。`UNLINK`(4.0+)在后台线程异步释放内存,**生产环境优先用 UNLINK**。
|
||
|
||
> [!WARNING] TTL 的陷阱
|
||
> - `TTL` 返回 `-1` 表示 key 存在但无过期时间,`-2` 表示 key 不存在
|
||
> - Redis 的过期键删除是**惰性删除 + 定期删除**混合策略,不能保证设置 EXPIRE 后立即清理
|
||
> - 集群模式下 `MOVE`、`RANDOMKEY`、`FLUSHALL` **不可用**
|
||
|
||
## SCAN vs KEYS 深度对比
|
||
|
||
```bash
|
||
# ⛔ KEYS — 阻塞整个服务
|
||
KEYS user:*
|
||
# 100 万 key 中匹配 1000 个 → 主线程阻塞数百毫秒甚至秒级
|
||
|
||
# ✅ SCAN — 增量迭代,每次 O(1)
|
||
SCAN 0 MATCH user:* COUNT 100
|
||
# 返回游标 + 一小批匹配 key → 用游标继续,直到游标回到 0
|
||
```
|
||
|
||
```go
|
||
// Go 中的 SCAN 用法
|
||
iter := rdb.Scan(ctx, 0, "user:prefix:*", 100).Iterator()
|
||
for iter.Next(ctx) {
|
||
fmt.Println(iter.Val())
|
||
}
|
||
// 最后检查 iter.Err()
|
||
```
|
||
|
||
> [!WARNING] SCAN 的不确定性
|
||
> - 某个 key 可能返回多次(中间被修改或删除)
|
||
> - 某些匹配的 key 可能不返回
|
||
> - 因此 **不要用 SCAN 做统计计数**,只能用于清理或迁移等容错场景
|
||
|
||
## 管道 Pipeline vs 事务 Transaction
|
||
|
||
### 先理解问题:为什么要"打包"命令?
|
||
|
||
Redis 通信遵循**一问一答**(request/response)模式——客户端发一条命令,等回复,再发下一条。每次"发送 → 等待 → 接收"的网络往返叫做一次 **RTT**(Round-Trip Time,往返时延)。
|
||
|
||
> [!QUESTION] RTT 有多贵?
|
||
> 假设内网 RTT = 0.5ms,发送 1000 条命令逐条执行需要 500ms。如果把它们**打包一次发送**,只需要 ~0.5ms——**性能差 1000 倍**。
|
||
|
||
Pipeline 和 Transaction 都是"打包"方案,但它们解决的问题不同:
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["客户端发送 N 条命令"] --> B{"核心诉求?"}
|
||
B -- "快!" --> C["Pipeline<br/>减少网络往返"]
|
||
B -- "安全!" --> D["Transaction<br/>保证命令原子执行"]
|
||
```
|
||
|
||
### Pipeline —— 批量发送,减少 RTT
|
||
|
||
Pipeline 的原理很简单:客户端先把 N 条命令存在本地缓冲区,**一次性**发送给 Redis 服务端,服务端依次执行后把 N 个回复**一次性**返回。
|
||
|
||
> [!NOTE] Pipeline 的两个关键点
|
||
> 1. **不保证原子性**——Pipeline 中的命令仍然可能被其他客户端的命令穿插执行
|
||
> 2. **不关心业务逻辑**——它纯粹是一个"网络优化层",减少的是等待时间
|
||
|
||
```go
|
||
pipe := rdb.Pipeline()
|
||
pipe.Set(ctx, "k1", "v1", 0)
|
||
pipe.Set(ctx, "k2", "v2", 0)
|
||
pipe.Set(ctx, "k3", "v3", 0)
|
||
results, err := pipe.Exec(ctx) // 一次性发送,一次性接收所有结果
|
||
```
|
||
|
||
```bash
|
||
# CLI 模式
|
||
redis-cli --pipeline <<< $'SET k1 v1\nSET k2 v2\nSET k3 v3'
|
||
```
|
||
|
||
> [!TIP] Pipeline 不是银弹
|
||
> 单次 batch 控制在 50~200 条为佳。过大反而会增加 Redis 服务端的内存压力和等待时间。
|
||
|
||
### MULTI / EXEC —— 原子化执行
|
||
|
||
事务解决的是另一个问题:**我想让一组命令"一起执行",中间不能被别人插队**。
|
||
|
||
`MULTI` → `命令1` → `命令2` → `EXEC` 的执行流程是:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as "客户端"
|
||
participant S as "Redis 服务端"
|
||
C->>S: "MULTI 开启事务"
|
||
S-->>C: OK
|
||
C->>S: "SET acc:A 900"
|
||
S-->>C: "QUEUED 排队,不立即执行"
|
||
C->>S: "SET acc:B 1100"
|
||
S-->>C: QUEUED
|
||
C->>S: "EXEC 提交"
|
||
S-->>C: "[OK, OK] 依次执行,一次性返回"
|
||
```
|
||
|
||
> [!QUESTION] "QUEUED" 是什么意思?
|
||
> `MULTI` 之后、`EXEC` 之前的命令**不会立即执行**,而是进入服务端的队列。只有收到 `EXEC` 后,Redis 才会**依次**执行队列中的所有命令,并把结果一次性返回。
|
||
|
||
```go
|
||
pipe, cancel := rdb.TxPipeline() // TxPipeline = Transaction Pipeline,即 MULTI/EXEC 包裹的 Pipeline
|
||
defer cancel()
|
||
|
||
pipe.Set(ctx, "acc:A", "900", 0)
|
||
pipe.Set(ctx, "acc:B", "1100", 0)
|
||
_, err := pipe.Exec(ctx) // 触发 EXEC,所有命令原子执行
|
||
```
|
||
|
||
```bash
|
||
# Redis CLI 中的事务
|
||
MULTI
|
||
> SET acc:A 900 # QUEUED
|
||
> SET acc:B 1100 # QUEUED
|
||
EXEC # 原子提交
|
||
```
|
||
|
||
### WATCH —— 乐观锁(防止"并发踩踏")
|
||
|
||
事务保证了"不插队",但如果另一个客户端在我 `WATCH` 到 `EXEC` 之间修改了数据怎么办?`WATCH` 就是为此设计的——它在 `EXEC` 前检查被监视的 key 是否被改过,**如果改了就自动放弃整个事务**。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as "客户端 A"
|
||
participant S as "Redis 服务端"
|
||
participant B as "客户端 B"
|
||
A->>S: WATCH stock:1001
|
||
A->>S: MULTI
|
||
A->>S: DECR stock:1001
|
||
B->>S: "SET stock:1001 999 被其他客户端修改"
|
||
A->>S: EXEC
|
||
S-->>A: "nil 事务被取消,stock:1001 已变"
|
||
```
|
||
|
||
> [!NOTE] WATCH 的使用范式
|
||
> 1. `WATCH key` —— 监视目标 key
|
||
> 2. 读取 key 的值,在客户端做判断
|
||
> 3. `MULTI` → 写命令 → `EXEC`
|
||
> 4. 如果 `EXEC` 返回 `nil`,说明数据被改过,需要**重试整个流程**
|
||
>
|
||
> 这就是经典的 **CAS(Compare-And-Swap)** 模式。实际开发中,Lua 脚本通常更简洁可靠。
|
||
|
||
### 对比总结
|
||
|
||
| 维度 | Pipeline | Transaction (MULTI/EXEC) |
|
||
|------|----------|------------------------|
|
||
| **核心目的** | 减少网络往返(**性能优化**) | 保证命令连续执行(**一致性**) |
|
||
| **原子性** | ❌ 不保证 | ✅ 保证(不会被其他命令插队) |
|
||
| **命令失败处理** | 单个失败不影响其他命令 | 语法错误 → 整个事务取消;运行时错误 → 仅该命令失败,其他继续 |
|
||
| **WATCH 支持** | ❌ | ✅ 乐观锁 |
|
||
| **适用场景** | 批量写入、批量查询 | 转账、库存扣减等需要原子性的操作 |
|
||
| **性能开销** | 几乎无额外开销 | 略高于 Pipeline(需要服务端队列) |
|
||
|
||
> [!WARNING] Redis 事务不是"数据库事务"
|
||
> 传统数据库的事务有 **ACID** 保证,失败会回滚。Redis 事务只保证**不中断**(队列中的命令会全部执行),中间命令出错**不会回滚**。如果需要"出错就回滚",请使用 Lua 脚本。
|
||
|
||
## Pub/Sub — 发布订阅
|
||
|
||
Pub/Sub 是 Redis 内置的消息广播模型:发布者向 channel 发消息,所有订阅者**实时**收到。注意它是 **fire-and-forget**——不持久化、不确认、离线消息会丢失。
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `SUBSCRIBE channel [channel ...]` | messages | O(N) | 订阅一个或多个 channel |
|
||
| `UNSUBSCRIBE channel [channel ...]` | OK | O(N) | 取消订阅 |
|
||
| `PUBLISH channel message` | receivers count | O(N+M) | 发布消息,N=channel 订阅者数,M=pattern 匹配数 |
|
||
| `PSUBSCRIBE pattern [pattern ...]` | messages | O(N) | 按 glob 模式订阅(如 `news.*`) |
|
||
| `PUNSUBSCRIBE pattern [pattern ...]` | OK | O(N) | 取消模式订阅 |
|
||
| `PUBSUB CHANNELS [pattern]` | channels | O(N) | 列出活跃 channel |
|
||
| `PUBSUB NUMSUB [channel ...]` | channel-count pairs | O(N) | 查询各 channel 订阅者数量 |
|
||
|
||
```go
|
||
// 订阅端——监听消息
|
||
sub := rdb.Subscribe(ctx, "order:events")
|
||
ch := sub.Channel()
|
||
for msg := range ch {
|
||
fmt.Printf("channel=%s payload=%s\n", msg.Channel, msg.Payload)
|
||
}
|
||
|
||
// 发布端——广播事件
|
||
receivers, _ := rdb.Publish(ctx, "order:events", `{"orderId":"1001","status":"paid"}`).Result()
|
||
fmt.Printf("delivered to %d subscribers\n", receivers)
|
||
```
|
||
|
||
> [!QUESTION] Pub/Sub vs Stream,怎么选?
|
||
> - **Pub/Sub**:实时广播,不关心历史,用完即丢(适合通知、缓存失效广播)
|
||
> - **Stream**:持久化消息队列,支持消费组、ACK、回溯(适合业务事件、任务队列)
|
||
> - 简单规则:**需要"至少一次"保证就用 Stream,否则 Pub/Sub 足够**
|
||
> - 两者命令对比详见 [[hhs/Redis/15-Stream]]
|
||
|
||
## Stream — 消息队列
|
||
|
||
Redis 5.0 引入的日志型数据结构,弥补了 Pub/Sub 不持久化的缺陷。底层用 Radix Tree + Listpack 实现,支持消费组(Consumer Group)和消息确认(ACK),可胜任轻量级任务队列。
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `XADD key [MAXLEN ~ n] ID field value [field value ...]` | entry ID | O(1) | 追加消息,`*` 自动生成 ID(时间戳-序号) |
|
||
| `XLEN key` | length | O(1) | 消息总数 |
|
||
| `XRANGE key start end [COUNT n]` | entries | O(N) | 按 ID 范围正序读取(`-`/`+` 表示最小/最大) |
|
||
| `XREVRANGE key end start [COUNT n]` | entries | O(N) | 按 ID 范围倒序读取 |
|
||
| `XREAD [COUNT n] [BLOCK ms] STREAMS key [key ...] ID [ID ...]` | entries | O(N) | 读取消息,`$` 表示仅新消息,BLOCK 阻塞等待 |
|
||
| `XGROUP CREATE key groupname ID [MKSTREAM]` | OK | O(1) | 创建消费者组,`$` 只消费新消息,`0` 从头消费 |
|
||
| `XREADGROUP GROUP group consumer [COUNT n] [BLOCK ms] [NOACK] STREAMS key [key ...] ID` | entries | O(N) | 组内消费,`>` 表示未分配的新消息 |
|
||
| `XACK key group ID [ID ...]` | acked count | O(N) | 确认消费,释放消息 |
|
||
| `XDEL key ID [ID ...]` | deleted count | O(N) | 删除指定消息 |
|
||
| `XTRIM key MAXLEN [~] n` | trimmed count | O(N) | 裁剪流长度,`~` 近似裁剪(性能更好) |
|
||
| `XPENDING key group [start end count] [consumer]` | pending info | O(N) | 查看未确认(pending)消息 |
|
||
| `XCLAIM key group consumer min-idle-time ID [ID ...]` | entries | O(N) | 认领超时未 ACK 的消息(故障转移) |
|
||
| `XINFO STREAM key` | stream info | O(1) | 查看流元信息 |
|
||
| `XINFO GROUPS key` | group info | O(N) | 查看所有消费者组 |
|
||
|
||
```go
|
||
// 生产者——追加消息
|
||
id, _ := rdb.XAdd(ctx, &redis.XAddArgs{
|
||
Stream: "order:events",
|
||
MaxLen: 10000, // 近似裁剪,控制内存
|
||
Approx: true,
|
||
Values: map[string]interface{}{"orderId": "1001", "status": "paid"},
|
||
}).Result()
|
||
fmt.Println("entry ID:", id)
|
||
|
||
// 消费者组——组内竞争消费
|
||
msgs, _ := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
|
||
Group: "order-consumers",
|
||
Consumer: "worker-1",
|
||
Streams: []string{"order:events", ">"},
|
||
Count: 10,
|
||
Block: 5 * time.Second,
|
||
}).Result()
|
||
|
||
for _, stream := range msgs {
|
||
for _, msg := range stream.Messages {
|
||
// 处理消息...
|
||
rdb.XAck(ctx, "order:events", "order-consumers", msg.ID) // 确认消费
|
||
}
|
||
}
|
||
```
|
||
|
||
> [!WARNING] 消息丢失的两个陷阱
|
||
> 1. **生产端**:`XADD` 返回后消息已写入内存,但若 Redis 崩溃且未配置 AOF,消息会丢失。对可靠性要求高的场景请开启 `appendfsync everysec`
|
||
> 2. **消费端**:`XREADGROUP` 读取后消息进入 Pending List(PEL),必须显式 `XACK` 才算消费完成。忘记 ACK 会导致消息堆积在 PEL 中,需要用 `XPENDING` + `XCLAIM` 清理
|
||
|
||
> [!TIP] MAXLEN vs MINID
|
||
> - `XADD key MAXLEN ~ 10000`:限制流最大约 10000 条(近似裁剪,性能好)
|
||
> - `XADD key MINID ~ 1685000000000-0`:限制最小消息 ID(按时间裁剪,7.0+)
|
||
> - 两者都建议加 `~` 做近似裁剪,避免精确裁剪带来的 O(N) 阻塞
|
||
|
||
## Geo — 地理位置
|
||
|
||
Redis 3.2+ 引入的地理位置类型,底层用 **ZSet** 实现(GeoHash 编码为 score),支持范围查询和距离计算。
|
||
|
||
| 命令 | 返回值 | 复杂度 | 说明 |
|
||
|------|--------|--------|------|
|
||
| `GEOADD key longitude latitude member [lon lat member ...]` | added count | O(log N) | 添加地理位置 |
|
||
| `GEOPOS key member [member ...]` | coordinates | O(N) | 获取坐标 |
|
||
| `GEODIST key member1 member2 [m\|km\|ft\|mi]` | distance | O(1) | 两点间距离 |
|
||
| `GEORADIUS key lon lat radius m\|km\|ft\|mi [WITHCOORD] [WITHDIST] [ASC\|DESC] [COUNT n]` | members | O(N+log N) | 以坐标为中心搜索 ⚠️ 6.2+ 已废弃,用 GEOSEARCH |
|
||
| `GEORADIUSBYMEMBER key member radius m\|km\|ft\|mi` | members | O(N+log N) | 以成员为中心搜索 ⚠️ 6.2+ 已废弃 |
|
||
| `GEOSEARCH key FROMLONLAT lon lat BYRADIUS radius m\|km [ASC\|DESC] [COUNT n] [WITHCOORD] [WITHDIST]` | members | O(N+log N) | ✅ 6.2+ 推荐,统一搜索命令 |
|
||
| `GEOSEARCH key FROMMEMBER member BYRADIUS radius m\|km` | members | O(N+log N) | 以成员为中心搜索 |
|
||
| `GEOSEARCHSTORE dst src ...` | stored count | O(N+log N) | 搜索结果存入新 key(6.2+) |
|
||
| `GEOHASH key member [member ...]` | geohash strings | O(N) | 返回 GeoHash 编码字符串 |
|
||
|
||
```go
|
||
// 门店入库——添加地理位置
|
||
rdb.GeoAdd(ctx, "stores:shanghai",
|
||
&redis.GeoLocation{Name: "store-001", Longitude: 121.4737, Latitude: 31.2304},
|
||
&redis.GeoLocation{Name: "store-002", Longitude: 121.4897, Latitude: 31.2397},
|
||
)
|
||
|
||
// 附近 3km 的门店——GEOSEARCH
|
||
stores, _ := rdb.GeoSearchLocation(ctx, "stores:shanghai", &redis.GeoSearchLocationQuery{
|
||
GeoSearchQuery: redis.GeoSearchQuery{
|
||
Longitude: 121.4800,
|
||
Latitude: 31.2350,
|
||
Radius: 3,
|
||
RadiusUnit: "km",
|
||
Sort: "ASC",
|
||
Count: 10,
|
||
},
|
||
WithCoord: true,
|
||
WithDist: true,
|
||
}).Result()
|
||
```
|
||
|
||
> [!TIP] GEO 底层就是 ZSet
|
||
> 可以用 `ZRANGE`、`ZREM` 等 ZSet 命令操作 Geo key。`GEODIST` 计算的是球面距离(Haversine 公式),精度在 ~0.5% 以内。
|
||
|
||
## Lua 脚本速查
|
||
|
||
Redis 2.6+ 内置 Lua 解释器,脚本在服务端**原子执行**,是实现 CAS、限流器、排行榜原子操作的利器。
|
||
|
||
| 命令 | 返回值 | 说明 |
|
||
|------|--------|------|
|
||
| `EVAL "script" numkeys key [key ...] arg [arg ...]` | script result | 执行 Lua 脚本 |
|
||
| `EVALSHA sha1 numkeys key [key ...] arg [arg ...]` | script result | 按 SHA1 执行(脚本已缓存) |
|
||
| `SCRIPT LOAD "script"` | SHA1 | 缓存脚本,返回 SHA1 |
|
||
| `SCRIPT EXISTS sha1 [sha1 ...]` | 1 or 0 array | 检查脚本是否已缓存 |
|
||
| `SCRIPT FLUSH` | OK | 清空脚本缓存 |
|
||
| `SCRIPT KILL` | OK | 终止正在执行的脚本(只限无写操作时) |
|
||
|
||
```go
|
||
// 原子扣减库存——Lua 实现 check-and-set
|
||
script := redis.NewScript(`
|
||
local stock = tonumber(redis.call('GET', KEYS[1]))
|
||
if stock and stock >= tonumber(ARGV[1]) then
|
||
redis.call('DECRBY', KEYS[1], ARGV[1])
|
||
return 1
|
||
end
|
||
return 0
|
||
`)
|
||
result, _ := script.Run(ctx, rdb, []string{"stock:sku:1001"}, 1).Int()
|
||
// result == 1 表示扣减成功,0 表示库存不足
|
||
```
|
||
|
||
> [!WARNING] Lua 脚本的限制
|
||
> - 脚本执行期间**阻塞整个 Redis**,严禁写耗时逻辑(循环上限建议 < 5000 次迭代)
|
||
> - 脚本中不能访问外部网络或文件系统
|
||
> - 集群模式下所有 KEYS 必须在同一个 slot(用 `{hash_tag}` 保证)
|
||
> - `SCRIPT KILL` 只能终止**未执行写操作**的脚本;已写数据的脚本只能等它跑完或重启 Redis
|
||
|
||
> [!NOTE] EVAL vs EVALSHA
|
||
> `EVAL` 每次发送完整脚本源码,`EVALSHA` 只发送 40 字节的 SHA1 摘要。高并发场景**务必用 EVALSHA + SCRIPT LOAD**,减少网络开销。go-redis 的 `NewScript` 会自动处理 EVALSHA 失败后回退 EVAL。
|
||
|
||
## 关联笔记
|
||
|
||
- [[02-核心数据类型]] — 每种类型的底层编码原理(ziplist → hashtable 切换机制)
|
||
- [[02-核心数据类型/02-2-skiplist]] — ZSet 底层跳表实现,理解 ZRANGE 为什么是 O(log N+M)
|
||
- [[04-RDB持久化]] / [[05-AOF持久化]] — 持久化策略对命令性能的影响
|
||
- [[08-SortedSet精解]] — ZSet 进阶玩法:排行榜、延迟队列、优先级队列
|
||
- [[09-高级特性]] — Pipeline / Lua / 事务深入解析
|
||
- [[15-Stream]] — Stream 完整指南:Consumer Group 模式、消息确认、故障转移
|
||
- [[16-GEO]] — Geo 底层原理与围栏检测实战
|