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

Redis MULTI EXEC 无回滚时的业务补偿方案

来源:17golang原创

时间:2026-10-16 22:58:32 158浏览 收藏

Redis 的 MULTI/EXEC 不提供关系型数据库那种“执行到一半自动回滚”。它保证事务中的命令按提交顺序连续执行,但执行期的类型错误、状态错误仍会分别返回;已经成功的命令不会因为后面的命令报错而撤销。工程上更稳妥的做法是:把能提前发现的错误放到 WATCH 后、MULTI 前检查,把不可逆的业务动作记录成幂等状态,并让补偿任务依据状态重试,而不是盲目重放整段事务。

官方地址:https://redis.io/docs/latest/develop/using-commands/transactions/

先把 Redis 事务当成“串行提交 + 逐条结果”,再用业务状态机补齐回滚语义:Redis 负责原子地完成 Redis 内的那组命令,应用负责判断结果、记录失败和补偿外部动作。

先看清 MULTI/EXEC 到底保证了什么

进入 MULTI 后,后续命令先排队;调用 EXEC 时,Redis 会在一个连续的执行窗口里按原顺序处理它们。其他客户端不会插入到这组命令中间,但这不代表每条命令都能成功,更不代表失败后会恢复之前的值。

最容易混淆的是两类错误。命令不存在、参数数量不对等入队阶段就能识别的问题,可能让 EXEC 返回 EXECABORT,整组命令不会开始执行。依赖实际 key 类型或内容的错误,要到执行时才能发现;此时结果数组中的某一项可能是错误,而后续项仍然继续处理。

# 说明执行期错误:INCR 失败,不会自动撤销后面的 SET
MULTI
SET order:100 status:paid
INCR order:100
SET order:100:note "需要人工处理"
EXEC

# EXEC 的回复按入队顺序对应每一条命令;中间错误不是回滚信号
Redis MULTI EXEC 提交边界和逐条错误回复的静态结构说明图
图1:结构说明图,展示 Redis MULTI/EXEC 的提交边界与逐条结果关系,不是截图或运行证据。

为什么 EXEC 后还会出现部分错误

把一个事务拆成“状态写入、计数变化、通知标记”三类命令,就能看出补偿边界。Redis 可以保证这三条命令连续执行,却无法知道业务上“订单已支付但库存扣减失败”意味着什么,也不会替应用调用支付平台或发送消息。

因此,应用收到 EXEC 的数组回复后,要按位置和错误类型建立结果判断:全部成功才把业务状态置为完成;部分成功则写入待补偿状态,并保存请求幂等键、已完成动作和失败原因。不要只判断客户端调用有没有抛异常,因为执行期错误经常作为结果数组中的一项返回。

把可预防错误放到提交前

如果事务依赖当前值,先用 WATCH 监视相关 key,再读取并检查类型、版本和业务状态,最后才进入 MULTI。被监视 key 在这期间发生变化时,EXEC 会返回空结果,应用应重新读取并重试;这属于并发冲突,不是需要补偿的部分提交。

# 读取和校验发生在 MULTI 之前,避免把明显的类型错误带进队列
WATCH stock:{sku}:count
GET stock:{sku}:count
TYPE stock:{sku}:count

# 应用确认数量为合法整数且状态允许扣减后,再提交写入
MULTI
DECRBY stock:{sku}:count 1
HSET order:{id} status reserving
EXEC

# EXEC 返回空结果时表示 WATCH 冲突,应重新读取;不要直接补偿旧快照

WATCH 解决的是“提交前观察到的 key 被改动”这一类竞争问题,不会把执行期类型错误变成回滚,也不能保护 Redis 之外的副作用。对复杂校验,可以把校验结果和业务版本一起写入状态记录,让重复请求仍能得到同一个结果。

用业务状态承接补偿

跨越 Redis、消息队列、库存服务或支付服务时,补偿对象必须是业务动作,而不是 Redis 命令本身。可以为每个请求保存一个状态,例如 pending、committed、needs_compensation、compensated。补偿 worker 只领取未完成状态,并用请求幂等键避免重复扣库存或重复发通知。

# 伪代码展示状态边界;外部服务必须支持幂等键
if execRepliesAreAllSuccessful(replies) {
    // Redis 内的状态已完成,记录 committed,避免补偿任务重复处理
    saveState(requestID, "committed")
} else {
    // 保存已成功的动作和错误项,交给可重试的补偿任务
    saveState(requestID, "needs_compensation", replies)
}

// 补偿任务按 requestID 重试,并把最终结果写成 compensated 或人工介入
Redis WATCH 条件域与业务补偿记录、外部副作用关系的静态说明图
图2:关系说明图,展示 Redis 状态与业务补偿记录、外部副作用之间的边界,不是截图或运行证据。

重试、补偿和人工兜底怎么分

可以按失败位置分流。EXECABORT 说明队列阶段就有问题,应修正命令或参数,不能靠重试掩盖;EXEC 空结果说明 WATCH 冲突,适合限制次数的快速重试;返回数组中出现执行期错误时,先保存结果和幂等状态,再根据已成功动作生成补偿;外部服务超时则要等待对方通过幂等键查询或重试,不能凭一次超时就反向扣减。

现象含义处理方式
EXECABORT入队阶段发现错误修正命令,不执行补偿
EXEC 返回空WATCH key 在提交前变化重新读取,有限重试
回复数组含错误项执行期某条命令失败保存部分结果,进入补偿状态
外部调用超时结果未知用幂等键查询或重试,必要时人工介入

最后复查三件事:同一个请求重放不会产生第二次业务动作;补偿任务能够从中断位置继续;无法自动判断的未知状态会被保留,而不是被当成成功。这样设计后,Redis 事务承担的是清晰的串行执行责任,业务系统则拥有可观察、可重试、可收敛的失败处理路径。

相关问题

Redis MULTI/EXEC 能保证所有命令一起成功吗?

不能。它保证命令按顺序连续执行,但执行期错误可能只影响对应命令,后续命令仍会返回结果。

WATCH 冲突需要做业务补偿吗?

通常不需要补偿旧结果,因为事务尚未执行;应重新读取最新值并在有限次数内重试。

什么时候应该把补偿交给后台任务?

当失败涉及外部服务、消息投递或无法在当前请求内确认的状态时,保存幂等键和动作记录,再由后台任务重试更安全。

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