登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  Redis

Redis事务中WATCH实现乐观锁

来源:17golang原创

时间:2026-11-15 11:20:35 184浏览 收藏

Redis 事务里的 WATCH 适合解决“先读库存,再决定是否扣减”这类并发更新问题。它不会把其他客户端挡在门外,而是把 EXEC 变成有条件的提交:被监视的键在读取后到提交前发生变化,事务就放弃整组排队命令,应用重新读取并重试。

官方地址:https://redis.io/

下面用一个很小的 Go 库存扣减项目串起概念。示例依赖 go-redis/v9,重点是理解竞争失败如何返回,而不是把 WATCH 当成普通互斥锁。

先把 WATCH 的边界说清楚

Redis 事务通常由 MULTI、命令队列和 EXEC 组成。进入 MULTI 后,后续命令先排队,直到 EXEC 才执行。WATCH 位于读取和提交之间,监视一个或多个键;如果其中任意键在这段窗口内被修改,EXEC 会返回空结果,队列中的写入不会执行。

这就是乐观锁:默认冲突不频繁,先读取、准备写入,提交时再检查前提是否仍然成立。它不是阻塞式锁,也没有通用回滚机制;一个事务中某条命令执行报错时,其他已排队命令并不会自动回滚。

客户端读取、WATCH键、MULTI队列和EXEC条件与Redis键空间的静态关系说明图
图1:静态结构说明图,查看 WATCH 读取窗口与 EXEC 条件提交之间的边界。

准备一个可重试的库存扣减项目

先把库存放在一个字符串键里,例如 stock:sku-100。每次扣减都必须满足两个条件:读取到的库存仍然是刚才看到的版本,并且库存大于等于购买数量。重试次数要有上限,否则高冲突时请求会无限占用连接。

# 初始化一个演示库存键;真实环境请替换为受控的业务键
redis-cli SET stock:sku-100 10

# 安装 Redis 的 Go 客户端;go.mod 会记录依赖版本
go get github.com/redis/go-redis/v9

项目的关键不是“把所有命令塞进 MULTI”,而是先在 WATCH 保护的连接上读出当前值,再根据这个值构造事务。读操作不能放在 MULTI 后期待立即得到结果,因为那时它只会返回 QUEUED。

用 go-redis 把读取和提交连起来

go-redis 的 Watch 回调会把事务对象交给我们。回调里先用 tx.Get 读取库存,再用 TxPipelined 排队写入;如果监视键发生变化,客户端会把这次竞争报告为 redis.TxFailedErr。

package main

import (
    "context"
    "errors"
    "fmt"

    "github.com/redis/go-redis/v9"
)

// deductStock 只在读取到的库存仍未变化时提交扣减。
func deductStock(ctx context.Context, rdb *redis.Client, key string, quantity int64) error {
    const maxRetries = 5 // 冲突时最多重试,避免高并发下无限占用连接

    for attempt := 0; attempt 

这里的 TxFailedErr 代表“本次乐观锁前提失效”,不是 Redis 宕机,也不是库存不足。把这三类情况分开,调用方才能分别决定重试、提示用户或触发故障处理。

把竞争失败和真正错误分开处理

可以把一次调用想成两个边界:左侧是业务根据库存值准备写入的竞争域,右侧是连接、数据类型和业务规则组成的错误域。TxFailedErr 只属于前者,有限重试后仍失败就应该返回明确的“竞争过高”;连接超时、键类型错误和库存不足则直接保留原错误语义。

库存键、事务回调、TxFailedErr、有限重试与连接错误业务错误的静态边界图
图2:静态边界说明图,查看竞争失败重试与其他错误返回的分工。

还要注意三个边界:第一,Redis 文档说明 6.0.9 及以后,被 WATCH 的键因过期或淘汰而变化也可能让事务中止;第二,Redis Cluster 中 WATCH 涉及的键需要满足同槽约束,多键操作可使用相同 hash tag;第三,如果逻辑越来越复杂,Lua 脚本通常更适合把读取和写入放在 Redis 端一次完成。

项目验收与常见疑问

验收时只需要确认契约:库存键是字符串数字;成功路径返回 nil 并扣减一次;竞争路径识别 TxFailedErr 后重新读取;超过上限返回明确错误;其他错误不被包装成“重试成功”。

WATCH 会锁住 Redis 吗?不会。它记录要检查的键,其他客户端仍可访问;冲突在 EXEC 时体现为本次提交失败。

为什么不直接用 DECRBY?如果只做无条件扣减,DECRBY 足够;当扣减前还要检查库存、状态或另一个键时,才需要 WATCH、Lua 或其他原子设计。

记住一句话:WATCH 保护的不是整个业务流程,而是“读取到的前提仍然成立”这一小段条件窗口。把冲突当作正常分支、给重试设上限,才能让 Redis 乐观锁真正适合业务代码。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>