Files
cs-note/hhs/Redis/03-基本命令速查.md
T
2026-05-25 20:51:57 +08:00

558 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 底层原理与围栏检测实战