vault backup: 2026-05-21 23:55:49

This commit is contained in:
hhs
2026-05-21 23:55:49 +08:00
parent b2b947f9f4
commit 9cb53bad9c
+84 -15
View File
@@ -252,16 +252,36 @@ for iter.Next(ctx) {
## 管道 Pipeline vs 事务 Transaction
Redis 协议本身就是 request/response,Pipeline 把多个命令打包一次发送,大幅减少网络往返;而事务通过 `MULTI` / `EXEC` 保证一组命令的**原子执行**。
### 先理解问题:为什么要"打包"命令?
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) // 一次性发送,一次性接收
results, err := pipe.Exec(ctx) // 一次性发送,一次性接收所有结果
```
```bash
@@ -270,37 +290,86 @@ redis-cli --pipeline <<< $'SET k1 v1\nSET k2 v2\nSET k3 v3'
```
> [!TIP] Pipeline 不是银弹
> 单次 batch 控制在 50~200 条为佳。过大反而会增加 RTT 等待时间,且占用服务端协程。
> 单次 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()
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) // 全部命令依次执行(不被其他命令打断)
_, err := pipe.Exec(ctx) // 触发 EXEC,所有命令原子执行
```
```bash
# Redis CLI 中的事务
MULTI
> SET acc:A 900
> SET acc:B 1100
> SET acc:A 900 # QUEUED
> SET acc:B 1100 # QUEUED
EXEC # 原子提交
```
> [!NOTE] Pipeline vs Transaction 对比
> | 维度 | Pipeline | Transaction (MULTI/EXEC) |
> |------|----------|------------------------|
> | **目的** | 减少网络往返(性能) | 保证原子性(一致性) |
> | **命令失败处理** | 单个失败不影响其他命令 | `EXEC` 前语法错误会取消整个事务 |
> | **WATCH 支持** | 不支持 | 支持乐观锁 (`WATCH key`) |
> | **适用场景** | 批量写入、批量查询 | 转账、库存扣减等需要原子性的操作 |
### 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 事务不是"数据库事务"
> Redis 事务只保证**不中断**(没有回滚),中间执行的命令如果出错,后续命令仍会继续。如果需要"出错就回滚"的行为,请使用 Lua 脚本。
> 传统数据库的事务有 **ACID** 保证,失败会回滚。Redis 事务只保证**不中断**(队列中的命令会全部执行),中间命令出错**不会回滚**。如果需要"出错就回滚",请使用 Lua 脚本。
## Pub/Sub — 发布订阅