Redis事务本质是命令队列+原子提交,无自动回滚与隔离级别,go-redis/v9因此不提供BeginTx方法,仅通过TxPipeline和Watch显式实现CAS语义。

Go 标准库不支持 Redis 事务,必须依赖客户端库的 Multi / Exec 语义实现,且需手动处理 WATCH 失败重试。
为什么 redis.Client 没有 BeginTx 这类方法
Redis 本身没有传统数据库的“事务隔离级别”或自动回滚机制;它的 MULTI/EXEC 只是命令打包+原子提交,出错时不会自动终止后续命令(除非用 WATCH 检测 key 变更)。Go 的 github.com/go-redis/redis/v9 客户端因此不封装“事务对象”,而是暴露 TxPipeline 和显式 Watch 接口。
常见错误现象:client.TxPipelined 返回 nil 错误但实际命令已部分执行——因为 EXEC 失败只返回错误,不撤回已排队命令(Redis 本身就不支持)。
- 使用场景:扣减库存、转账、生成唯一 ID 等需要 CAS 语义的操作
- 关键区别:
client.Pipeline()是批量非事务,client.TxPipelined()才等价于MULTI+EXEC - 性能影响:每次
WATCH会增加一次 round-trip,高并发下失败率上升,必须配合指数退避重试
如何正确使用 client.Watch + client.TxPipelined
Redis 事务真正可靠的条件是“带 WATCH 的 EXEC”,否则只是命令队列。Go 客户端要求你在 Watch 回调里完成全部逻辑,并在回调内调用 client.TxPipelined。
示例:安全扣减库存(key = "stock:123")
err := client.Watch(ctx, func(tx *redis.Tx) error {
val, err := tx.Get(ctx, "stock:123").Int64()
if err != nil && err != redis.Nil {
return err
}
if val <= 0 {
return errors.New("insufficient stock")
}
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Decr(ctx, "stock:123")
pipe.Incr(ctx, "order:count")
return nil
})
return err
}, "stock:123")
-
Watch的最后一个参数是监控的 key 列表,任意一个被改写都会导致EXEC返回redis.TxFailedErr - 回调函数必须是纯逻辑:不能在外部读取 key 后再传入,否则存在竞态
- 如果
err == redis.TxFailedErr,需重试整个Watch调用(库不自动重试)
容易踩的坑:redis.Nil、pipeline 错误捕获、超时控制
三个高频问题:一是把 redis.Nil 当成真实错误中断流程;二是 TxPipelined 内部命令出错被吞掉;三是没设 ctx 超时导致阻塞。
-
GET不存在的 key 返回redis.Nil,必须用errors.Is(err, redis.Nil)判断,不能直接if err != nil -
pipe.Set(...)等命令的错误不会立即返回,要检查pipe.Exec()的返回值(TxPipelined内部已做) - 务必用带超时的
ctx:如ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second),否则WATCH长时间等待可能卡死 goroutine - 不要在
Watch回调外修改被监控的 key,包括其他 goroutine 或其他服务
Redis 事务的“原子性”只体现在 EXEC 阶段命令按序执行,不保证业务逻辑正确性;WATCH 失败重试逻辑必须由你控制,且重试次数要限制,否则可能引发雪崩。


















