导读:本期聚焦于Canve创作的《Redis事务MULTI和EXEC命令怎么用?深入解析Redis事务的正确用法与常见陷阱》,敬请观看详情。Redis事务的核心是MULTI和EXEC这对命令,但真正理解它工作原理的人并不多。Redis事务不同于MySQL的事务,它不保证原子性回滚,也不支持部分执行失败后的撤销操作。本文将详细讲解MULTI开启事务、命令入队、EXEC触发执行的完整流程,演示DISCARD取消事务和WATCH乐观锁的使用方法,分析Redis事务与Lua脚本在原子操作场景下的差异,并指出事务中命令语法错误与运行时错误的不同处理结果。掌握这些知识点,能帮助你在库存扣减、订单创建等业务场景中正确运用Redis事务。

Redis事务的核心是MULTI和EXEC这对命令,它允许开发者把多个命令打包成一个整体依次执行,执行期间不会被其他客户端的命令插入。不过Redis事务与MySQL等关系型数据库的事务存在本质区别:它不支持回滚,入队阶段出错的命令不会阻止其他命令执行。下面从基本用法、WATCH乐观锁、与Lua脚本的对比等角度,系统讲解Redis事务的正确使用方式。

Redis事务MULTI和EXEC命令怎么用?深入解析Redis事务的正确用法与常见陷阱

一、MULTI和EXEC的基本用法

Redis事务的执行分为三个阶段:首先使用MULTI命令开启事务,随后客户端发送的命令不会立即执行,而是返回QUEUED表示已加入队列;最后调用EXEC命令,Redis会按顺序执行队列中的所有命令,并把每条命令的执行结果按顺序返回。

来看一个完整的示例,演示一次性完成设置用户余额和记录日志两个操作:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET user:1001:balance 500
QUEUED
127.0.0.1:6379(TX)> INCRBY user:1001:balance 200
QUEUED
127.0.0.1:6379(TX)> GET user:1001:balance
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (integer) 700
3) "700"

从输出可以看到,前三条命令都返回QUEUED,说明它们只是被缓存到了队列里,真正执行发生在EXEC那一刻。执行期间即使有其他客户端并发请求,Redis也会保证整个队列连续执行完毕,这正是Redis事务的核心价值—— isolation隔离性。

如果想放弃一个已经开始的事务,可以使用DISCARD命令清空命令队列并退出事务状态。这在动态拼接命令、发现条件不满足时非常有用:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET stock:1001 50
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.0.0.1:6379> GET stock:1001
(nil)

二、Redis事务没有回滚:两类错误的不同表现

很多人以为事务出错会整体回滚,这是Redis事务最大的认知误区。官方文档明确说明:Redis事务不支持回滚,但会区分两种错误类型,它们的处理方式完全不同。

第一种是命令入队错误。如果某条命令本身存在语法问题,或者命令参数数量不对,入队时Redis就会直接报错。这种情况下,整个事务会被拒绝执行,EXEC会返回一个错误信息,队列中所有命令都不会运行,相当于事务整体失败:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET key1 "hello"
QUEUED
127.0.0.1:6379(TX)> WRONGCOMMAND key1
(error) ERR unknown command 'WRONGCOMMAND'
127.0.0.1:6379(TX)> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

第二种是运行时错误。命令语法正确并成功入队,但执行时因为类型不符等原因失败,比如对一个字符串类型的key执行LPUSH。这种情况下,只有出错的那条命令返回错误,队列中其他命令照常执行,之前已执行的命令结果也不会被撤销:

127.0.0.1:6379> SET name "redis"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET name "mysql"
QUEUED
127.0.0.1:6379(TX)> LPUSH name element
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> GET name
"mysql"

可以看到name的值已经被修改成了mysql,后面的LPUSH失败并不影响它。官方的解释是:回滚机制不能避免编程逻辑错误,而保留这种简单的设计能让Redis内部实现更简洁、执行速度更快。因此在使用事务时,务必在代码层面做好类型校验和业务校验。

三、WATCH乐观锁:让事务具备条件执行能力

MULTI和EXEC本身只提供隔离性,不具备条件判断能力。比如扣减库存时,我们需要先检查库存是否充足再执行扣减,但GET和DECRBY之间可能被其他客户端插入修改。这时就需要WATCH命令实现乐观锁。

WATCH的原理是:在MULTI之前对一个或多个key加监视,当EXEC执行时,Redis会检查这些key在事务期间是否被修改过(包括自身修改、过期删除、其他客户端修改)。只要任一被监视的key发生了变化,整个事务直接放弃执行,EXEC返回nil。典型的扣库存流程如下:

import redis

client = redis.Redis(host='127.0.0.1', port=6379)

def deduct_stock(product_id, quantity):
    key = f"stock:{product_id}"
    while True:
        with client.pipeline() as pipe:
            try:
                # 监视库存key,开启乐观锁
                pipe.watch(key)
                stock = int(pipe.get(key) or 0)
                if stock < quantity:
                    pipe.unwatch()
                    return False  # 库存不足
                # 开启事务并重新获取连接控制权
                pipe.multi()
                pipe.decrby(key, quantity)
                pipe.execute()
                return True
            except redis.WatchError:
                # 库存被其他客户端修改,重试整个流程
                continue

print(deduct_stock(1001, 2))

上面代码中的watch对应WATCH命令,multi对应MULTI,整个逻辑构成了CAS(Check And Set)模式:先检查、再执行、失败重试。当并发量非常高导致频繁出现WatchError时,说明冲突激烈,可以考虑改用Lua脚本来降低重试开销。

四、Redis事务与Lua脚本的对比选择

Redis从2.6版本开始支持EVAL命令执行Lua脚本,Lua脚本同样具备原子性,而且天然支持条件逻辑,不需要重试。两者在各维度上的差异可以用下面的表格概括:

对比维度MULTI/EXEC事务Lua脚本
原子性保证队列顺序执行,不被打断整个脚本作为单命令执行
条件逻辑不支持,需配合WATCH重试原生支持if判断
命令间数据传递无法读取中间结果做判断可读取中间结果灵活处理
集群兼容性要求所有key在同一slot同样要求key在同一slot
调试与维护简单直观需要维护脚本,可先用SCRIPT LOAD缓存

同样实现扣库存,Lua脚本写起来更简洁,一个脚本内完成检查和扣减:

local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
local quantity = tonumber(ARGV[1])
if stock >= quantity then
    redis.call('DECRBY', KEYS[1], quantity)
    return 1
else
    return 0
end

调用方式为EVAL script 1 stock:1001 2。选型建议很简单:如果命令之间没有依赖关系,只是批量执行,用MULTI/EXEC即可;如果需要根据中间结果做分支判断,或者高并发场景下WATCH重试成本高,优先使用Lua脚本。需要注意的是,在Redis Cluster环境下,无论事务还是脚本,涉及的所有key必须落在同一个slot上,通常通过hash tag(如key写成stock:{1001})来解决。

五、生产环境使用注意事项

第一,事务队列中命令过多会带来风险。所有命令在EXEC前都缓存在服务端内存中,如果入队了上万个命令,会长时间占用内存,也可能造成EXEC执行时长时间阻塞单线程的Redis。建议单次事务控制在几十条命令以内,大批量操作可拆分成多个事务或改用pipeline配合批量写入。

第二,区分事务和pipeline。pipeline是客户端行为,只是把多条命令一次性发送减少网络往返,不保证原子性;事务是服务端行为,保证队列连续执行。很多客户端库(如Jedis、redis-py)在pipeline中开启multi后两者会结合使用,既减少网络开销又获得事务语义,这是生产中常见的最佳实践。

第三,WATCH之后如果事务被放弃,记得重新执行WATCH再进入下一轮重试,否则新一轮监视是失效的。同时避免监视过多的key,被监视的key越多,冲突概率越大,重试次数也会随之上升。合理设计key粒度,把乐观锁的监视范围控制在业务必需的最小集合内,才能让Redis事务在高并发场景下稳定发挥作用。

Redis事务MULTI EXECRedis命令修改时间:2026-09-01 22:58:42

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。