【Redis从入门到精通】第55篇:Redis事务——MULTI/EXEC/DISCARD/WATCH详解 上一篇【第54篇】发布订阅实战——实时消息推送、聊天室、事件通知下一篇【第56篇】Redis事务的ACID分析——它到底算不算ACID事务如果你用过MySQL一定很熟悉BEGIN ... COMMIT ROLLBACK。Redis也有事务但它的性格跟MySQL完全不同——它更像是一个批量命令执行器而不是传统意义上的数据库事务。今天我们就来详细拆解Redis事务的四个命令MULTI、EXEC、DISCARD、WATCH看看它的实现原理和使用场景。Redis事务的四个命令Redis事务由以下四个命令组成命令功能类比SQLMULTI标记事务开始之后命令进入队列BEGIN TRANSACTIONEXEC执行事务队列中的所有命令COMMITDISCARD取消事务清空队列ROLLBACKWATCH监视一个或多个key实现乐观锁SELECT … FOR UPDATE但机制不同事务的工作流程Redis 事务执行时序图 Client Redis Server │ │ │── MULTI ───────────────────────│ 开启事务返回OK │── OK ──────────────────────────│ │ │ (命令入队不执行) │── SET key1 value1 ─────────────│ 入队 ✓ │── QUEUED ──────────────────────│ │ │ │── SET key2 value2 ─────────────│ 入队 ✓ │── QUEUED ──────────────────────│ │ │ │── INCR counter ────────────────│ 入队 ✓ │── QUEUED ──────────────────────│ │ │ │── GET key1 ────────────────────│ 入队 ✓ │── QUEUED ──────────────────────│ │ │ │── EXEC ────────────────────────│ 批量执行队列中的所有命令 │── [OK, OK, 1, value1] ─────│ 返回所有命令的结果 │ │关键点MULTI之后、EXEC之前所有命令都只是入队不会实际执行。Redis也不会校验命令的合法性比如类型错误。只有当EXEC被调用时才按顺序批量执行。命令入队阶段当客户端执行MULTI后客户端的状态会切换为事务模式。在此模式下每个命令都会被放入事务队列中。事务队列的实现在Redis的客户端结构中事务队列存储在client.mstate中// client 结构中的事务状态typedefstructclient{multiState mstate;// 事务状态// ...}client;typedefstructmultiState{multiCmd*commands;// 命令数组FIFO队列intcount;// 命令数量// ...}multiState;typedefstructmultiCmd{robj**argv;// 命令参数数组intargc;// 参数个数structredisCommand*cmd;// 命令结构指针}multiCmd;事务队列内存布局 client.mstate ┌──────────────────────────────────────────────────┐ │ count: 4 │ │ │ │ commands[0]: SET key1 value1 │ │ argv: [SET, key1, value1] │ │ cmd: setCommand │ │ │ │ commands[1]: SET key2 value2 │ │ argv: [SET, key2, value2] │ │ cmd: setCommand │ │ │ │ commands[2]: INCR counter │ │ argv: [INCR, counter] │ │ cmd: incrCommand │ │ │ │ commands[3]: GET key1 │ │ argv: [GET, key1] │ │ cmd: getCommand │ └──────────────────────────────────────────────────┘入队阶段的两种错误这是Redis事务中最容易踩坑的地方。入队阶段有两种截然不同的错误类型处理方式也完全不同。类型一命令格式错误语法错误如果入队的命令格式不对比如拼写错误、参数个数不对Redis会立即返回错误同时将客户端标记为REDIS_DIRTY_EXEC。# 示例命令拼写错误MULTI OK SET key value QUEUED INCR key1 key2 key3# INCR 只接受1个参数这是语法错误(error)ERR wrong number of argumentsforincrcommandGET key QUEUED EXEC(error)EXECABORT Transaction discarded because of previous errors.结果整个事务被丢弃一条命令语法错误所有命令都不执行。类型二运行时错误类型错误如果命令格式正确但运行时才发现类型不匹配Redis会在EXEC时执行到那条命令时报错但不影响其他命令的执行。# 示例运行时类型错误SET keyhelloOK MULTI OK SET keyworldQUEUED INCR key# world 不是数字运行时才报错QUEUED SET other_keyvalueQUEUED EXEC1)OK ← SET成功2)(error)ERR value is not an integer or out of range ← INCR失败3)OK ← SET成功# 检查结果:GET keyworld← SET成功了但INCR失败了key还是worldGET other_keyvalue← 其他命令正常执行结果出错的命令被跳过其他命令正常执行。Redis不会回滚。两种错误对比入队阶段两种错误对比 ┌──────────────────────────────────────────────────────┐ │ 语法错误入队时发现 │ │ │ │ MULTI → SET → INCR(参数错) → GET → EXEC │ │ OK QUEUED ERROR! QUEUED EXECABORT! │ │ │ │ 结果: 所有命令都不执行 │ └──────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────┐ │ 运行时错误EXEC时发现 │ │ │ │ MULTI → SET → INCR(类型错) → SET → EXEC │ │ OK QUEUED QUEUED QUEUED 执行... │ │ OK │ │ ERROR │ │ OK │ │ │ │ 结果: 出错命令跳过其他命令正常执行 │ └──────────────────────────────────────────────────────┘踩坑提示这是Redis事务和MySQL事务最大的区别之一。MySQL会回滚所有操作Redis不会。所以在MULTI之前就要确保命令是正确的特别是INCR、ZADD这类对值类型有要求的命令。DISCARD 命令DISCARD用于取消当前事务MULTI OK SET key1 value1 QUEUED SET key2 value2 QUEUED# 改变主意了取消事务DISCARD OK# 此时事务队列已清空客户端回到正常模式# key1 和 key2 都没有被设置DISCARD做了三件事清空事务队列中的所有命令清除客户端的REDIS_MULTI标志如果有WATCH监视也一并取消WATCH 命令——乐观锁WATCH是Redis事务中最有意思的命令。它实现了**乐观锁Optimistic Locking**机制让你可以在事务执行前检查某个key是否被修改过。WATCH 的工作流程WATCH 乐观锁流程 Client-A Redis Client-B │ │ │ │── WATCH balance ──────────────│ │ │── OK ─────────────────────────│ │ │ │ │ │── GET balance ────────────────│ │ │── 1000 ─────────────────────│ │ │ │ │ │── MULTI ──────────────────────│ │ │── OK ─────────────────────────│ │ │── DECRBY balance 100 ─────────│ (入队) │ │── QUEUED ─────────────────────│ │ │ │ │ │ │← SET balance │ │ │ 500 ──────│ │ │← (balance被 │ │ │ Client-B修改)│ │ │ │ │── EXEC ────────────────────────│ │ │── nil ────────────────────────│ │ │ (事务被取消balance │ │ │ 在WATCH后被修改了) │ │ │ │ │WATCH 的实现原理Redis在服务端维护了一个watched_keys字典// watched_keys 字典结构// Key: 被监视的Redis key// Value: 监视该key的客户端链表dict*watched_keys;// 示例:// balance → [Client-A, Client-C]// stock → [Client-B]watched_keys 字典 ┌────────────┬──────────────────────────┐ │ Key │ Value (客户端链表) │ ├────────────┼──────────────────────────┤ │ balance │ [Client-A] → [Client-C] │ │ stock │ [Client-B] │ └────────────┴──────────────────────────┘ 当 balance 被修改时: ① 遍历 [Client-A, Client-C] ② 给每个客户端设置 REDIS_DIRTY_CAS 标志 ③ EXEC时检查该标志如果被设置则返回nilCAS 操作的代码示例# 场景实现一个安全的转账A给B转100元# --- 不使用WATCH不安全---GET balance:A1000# 如果此时别人也在操作A的余额...MULTI DECRBY balance:A100INCRBY balance:B100EXEC# 可能导致A的余额变成负数# --- 使用WATCH安全---WATCH balance:A OK GET balance:A1000# 检查余额是否足够MULTI DECRBY balance:A100INCRBY balance:B100EXEC# 如果EXEC返回nil说明balance:A在我们检查之后被修改了# 此时需要重试整个操作# --- 完整的CAS重试逻辑伪代码---def transfer(from_account, to_account, amount):whileTrue: WATCH from_account balanceGET from_accountifint(balance)amount: UNWATCHreturn余额不足MULTI DECRBY from_account amount INCRBY to_account amount resultEXECifresult is not None:return转账成功# EXEC返回nil说明被其他客户端修改了重试踩坑提示WATCH MULTI EXEC 只能保证如果key没变就执行变了就不执行。它不保证事务的完整性如前所述运行时错误不会回滚。所以WATCH适合做CAS操作但不适合需要严格一致性的场景。事务在集群模式下的限制在Redis Cluster中事务有一个重要的限制事务中的所有Key必须落在同一个槽位。# 集群模式下:# key user:1001 的槽位: 14520# key user:1002 的槽位: 12302MULTI SET user:1001张三QUEUED SET user:1002李四(error)CROSSSLOT Keysinrequest donthashto the same slot# 解决方案使用 {} 哈希标签MULTI SET{user}:1001张三# 槽位由user计算QUEUED SET{user}:1002李四# 槽位由user计算QUEUED EXEC1)OK2)OK# ✓ 成功因为 {user} 确保了两个key在同一个槽位MULTI/EXEC 嵌套的限制Redis不支持嵌套事务。如果你在事务中又执行了MULTI会得到错误MULTI OK SET key1 value1 QUEUED MULTI# 试图嵌套事务(error)ERR MULTI calls can not be nested EXEC1)OK如果你确实需要在事务中做条件判断请使用Lua脚本下一篇会详细介绍Lua脚本天然支持条件逻辑和原子性。事务 vs 普通命令的性能# 对比测试1000次SET操作# 方式1: 逐条发送redis-benchmark-tset-n1000-c1# 约 10000 requests/sec# 1000次网络往返# 方式2: 事务MULTI ... EXEC# 使用 pipeline MULTI/EXECredis-benchmark-tset-n1000-P1-c1# 约 20000 requests/sec# 只需1次网络往返# 方式3: Pipeline不含事务redis-benchmark-tset-n1000-P1000-c1# 约 100000 requests/sec# 1次网络往返1000条命令事务的主要价值是减少网络往返RTT而不是原子性。如果你只需要减少RTTPipeline可能比事务更高效。本章小结Redis 事务核心要点 ┌─────────────────────────────────────────────┐ │ │ │ MULTI → 开启事务命令入队 │ │ EXEC → 批量执行返回所有结果 │ │ DISCARD → 取消事务清空队列 │ │ WATCH → 乐观锁监视key变化 │ │ │ │ 语法错误 → EXECABORT全部不执行 │ │ 运行时错误 → 跳过错误命令其余正常执行 │ │ 集群模式 → 所有key必须在同一槽位 │ │ 不支持嵌套 → MULTI不能嵌套 │ │ │ │ 本质: 批量命令执行器 乐观锁 │ │ 不是: 传统ACID事务 │ │ │ └─────────────────────────────────────────────┘操作MULTI/EXECWATCH适用场景批量执行✓-减少网络往返条件执行✓✓CAS操作错误回滚✗-需要回滚请用Lua嵌套事务✗-用Lua脚本替代条件判断✗-用Lua脚本替代上一篇【第54篇】发布订阅实战——实时消息推送、聊天室、事件通知下一篇【第56篇】Redis事务的ACID分析——它到底算不算ACID事务