1. 从“Redis事务到底是不是事务”说起先丢一个很多面试官爱问、很多同学也容易翻车的问题Redis事务和MySQL事务到底是不是一回事先说结论——它们根本不在一个维度上。MySQL事务要的是ACID尤其是隔离性和持久性Redis事务玩的是“批量排队、顺序执行、部分回滚”这套逻辑。把它理解成“打包一批命令、确保按顺序执行”会更准确。很多人刚接触Redis时都会有这么个假设既然都叫事务那应该也支持回滚吧结果一测试发现第二条命令语法没错但运行报错时第一条命令已经写进去了回滚根本没发生。那一刻的困惑我特别理解因为这跟关系型数据库里“要么全成功、要么全失败”的直觉是冲突的。Redis事务的本质是命令入队阶段如果语法正确就全部排进一个队列执行阶段按顺序逐个跑中间某个命令运行时出错不会影响前后命令的执行。注意“不会影响”这四个字是整个理解的关键。但这不代表Redis事务没用。它真正解决的是“一批命令需要在一起执行、又不希望半路插进来别的客户端命令”的场景。换句话说Redis事务的核心价值是隔离而不是原子性。把这两点分开理解后面看源码、看面试题、看实际应用都会顺很多。这篇文章我会从底层执行机制讲起把MULTI、EXEC、DISCARD、WATCH这几个命令一个个拆开再配合实际场景说清楚什么情况该用、什么情况不该用最后把我自己调试时踩过的坑和面试里比较刁钻的问法一起整理出来。不管你是刚准备面试的新手还是已经在生产环境摸过Redis的老手这篇文章应该都能给你一些新的角度。2. 拆开Redis事务的底盖一条命令从MULTI到EXEC到底经历了什么2.1 三个核心状态排队、执行、作废Redis事务的实现本质上就是客户端与服务器之间的一种特殊交互状态。在没有开启事务时你发一条命令服务器解析完就执行然后返回结果但一旦你发送了MULTI事情就不一样了。从MULTI接收到EXEC之间后续命令不会立即执行而是被放进一个队列里。这个队列在Redis内部被称为事务队列每条命令都保存了参数和对应的执行函数指针等待EXEC发出后统一消费。我习惯把这个过程类比成去餐厅点餐。你坐下后先告诉服务员“我要点菜了”MULTI然后把菜一道道报给服务员命令入队服务员只是记录并不会冲进后厨去炒。最后你说“就这些上菜吧”EXEC后厨才开始按顺序做。中途如果不想吃了直接说“算了不要了”DISCARD服务员把单子一撕刚才报的菜全部作废。这个类比里还缺一个环节——WATCH。它相当于你跟服务员说“我坐的这桌有位子但如果在我点菜期间有人把这个位子占了那上菜前告诉我一声我取消订单。”WATCH命令就是这种“条件监视锁”监视一个或多个key如果在EXEC之前这些key被其他客户端修改过EXEC会直接返回空结果事务队列一条也不执行。从Redis源码的角度来看事务的关键结构是客户端对象里的mstate字段。这个字段保存着事务队列和队列长度。当你发送MULTI时客户端状态被标记为REDIS_MULTI后续命令通过processCommand函数进入队列逻辑。EXEC执行时会先检查WATCH的key是否发生过变更如果变更过则放弃执行如果没有变更就遍历队列逐条调用call函数去执行命令。整个过程单线程串行所以不存在并发交错的问题。这里要说一个非常重要的观念Redis事务不具备真正的原子性。操作系统的原子性定义是“要么全部发生、要么全不发生”Redis事务只能保证“执行过程中不会被其他客户端的命令打断”但执行中途如果某个命令报错前后的命令照样执行。这也是为什么圈子里常说“Redis事务是伪事务”。不过等一下语法错误和运行时错误的处理方式还不太一样我们接下来拆开看。2.2 语法错误和运行时错误两种完全不一样的命运面试里最常见的一个高分点是Redis事务里的命令出错到底会不会回滚很多人背答案说“不会回滚”但这个答案太粗糙了因为错误分两种情况。第一种是入队时报语法错误。比如你在MULTI之后发送了“SET key”这种缺少参数的命令Redis在命令入队时就能发现格式不对直接返回语法错误提示。这种情况下EXEC不会执行事务直接废弃所有命令都不执行。这在某种程度上很像“全失败”。第二种是入队时语法没问题但执行时因为类型不匹配、key不存在等运行时原因报错。比如你事务里有一条LPUSH但那个key其实是个字符串执行到这一步就会报WRONGTYPE错误。这种情况下其他命令不受影响该执行的继续执行。也就是说它是“部分成功、部分失败”完全不讲关系型数据库那套回滚逻辑。我刻意在本地测试过一次这种场景MULTISET key1 helloINCR key1此时key1是字符串INCR会报类型错误SET key2 worldEXEC执行结果就是key1被设置为helloINCR报错了但key2仍然被成功设置为world。如果按照MySQL事务的理解这显然是不可接受的。但站在Redis的设计哲学来看这又是合理的Redis追求的是极致的简单和性能如果要支持回滚就必须记录每一处数据的原始值、设计undo日志、处理嵌套恢复逻辑这会极大增加命令执行的复杂性收益却并不高。Redis官方文档甚至直言不讳命令错误通常是因为编程错误开发者在开发期就能发现不太需要在生产环境做复杂的回滚机制。理解这个点后你就会明白为什么“Redis事务不可靠”这种说法其实有点片面——它不是不可靠而是它的可靠性和你预期的不是一回事。它的可靠性体现在“串行、不被打断”上而非“出错全回滚”上。2.3 MULTI、EXEC、DISCARD、WATCH四条命令各管一段先看最基础的三个命令组合MULTI开启事务告诉Redis“接下来的命令先别执行排队去”。返回OK。EXEC触发事务真正执行Redis按顺序执行事务队列里的所有命令。这里有个细节EXEC之前的命令如果本身有语法错误EXEC会放弃执行全部命令如果只是运行时错误则只是该条命令报错其他照常执行。DISCARD取消事务清空事务队列并退出事务状态。它也会解除WATCH监视。然后是WATCH。WATCH是Redis事务中唯一带“条件”色彩的命令也是分布式场景下实现乐观锁的关键。它的工作原理是CASCompare and Swap的思想你在事务开始前WATCH一个或多个key相当于记下这些key的当前版本EXEC时Redis会检查这些key自被WATCH之后是否被修改过——注意哪怕是当前客户端自己修改的也会被检测到。一旦发现改动事务直接拒绝执行返回nil给EXEC。WATCH可以配合UNWATCH取消监视但这个命令用的不多因为DISCARD和EXEC都会自动解除WATCH。这里我想额外分享一个很容易被忽视的盲点WATCH监视的是key是否被“修改”而不是key的“最终值是否一样”。举个例子一个计数器从1改到2再改回1两次修改动作都会触发版本变化WATCH依然会认为这个key被改过EXEC依然会失败。这是因为Redis内部维护的version机制记录的是修改次数而不是具体值。所以如果你的业务逻辑是基于值的等值判断要特别注意这个“过程敏感性”。3. 核心实操五步吃透Redis事务的正确打开方式3.1 环境准备没有Redis环境一切都是空谈实操之前先把环境跑起来。我用的是Docker方式安装Redis这是目前最省事、也最不容易污染本机的方案。如果你还没有Docker可以先去官网装一个桌面版如果已经是老手直接用本机命令行启动redis-server也可以。Docker方式只需要一条命令docker run -d --name redis-dev -p 6379:6379 redis:7.0如果你对容器内的数据持久化有要求再加一个volume挂载把redis-server的RDB或AOF文件落到宿主机docker run -d --name redis-dev -p 6379:6379 -v /myredis/data:/data redis:7.0 --appendonly yes这里解释一下参数-d代表后台运行--name是给容器起名-p 6379:6379是端口映射--appendonly yes表示开启AOF持久化。AOF会让每条写命令追加到日志文件中这样即使容器重启数据也能找回至少不会因为容器销毁而痛哭流涕。如果你想练手WATCH和事务的并发效果单机Redis就够了。WATCH的失效检测是服务器端做的不需要搭建主从或者集群。如果你后续要玩分布式锁、主从切换那一套再考虑docker-compose或者k8s方案。别在环境上花太多时间重点是把命令跑明白。3.2 使用Redis命令行客户端亲手敲一遍事务流程启动Redis后打开一个终端进入redis-cli交互模式。如果你是Docker方式需要先进入容器docker exec -it redis-dev redis-cli然后依次执行下面这套脚本我建议你先自己手敲一遍再对照我的注释理解输出127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET article:1:title Redis事务解读 QUEUED 127.0.0.1:6379 SET article:1:author 张三 QUEUED 127.0.0.1:6379 INCR article:1:views QUEUED 127.0.0.1:6379 EXEC 1) OK 2) OK 3) (integer) 1可以看到MULTI之后每条命令的返回都是QUEUED而不是正常的执行结果。这个QUEUED就是把命令押进了事务队列。直到你发送EXEC服务器才按顺序执行这三条命令返回一个数组数组里每个元素对应队列中每条命令的执行结果。在上面这个例子里INCR article:1:views的返回是“(integer) 1”说明这个计数器从0变成了1。如果你在EXEC之前想反悔直接发DISCARD所有QUEUED的命令都不会执行。这个操作在调试点错命令时特别有用可以随时终止事务而不需要挨个撤销。而WATCH的用法是这样的127.0.0.1:6379 WATCH article:1:views OK 127.0.0.1:6379 MULTI OK 127.0.0.1:6379 INCR article:1:views QUEUED 127.0.0.1:6379 EXEC (nil)这里有个很经典的陷阱如果WATCH之后你自己在同一个连接里对article:1:views执行了修改那么EXEC也会返回nil。因为WATCH检测的是这个key的修改事件而被监视的key已经被动过事务自然不会再执行。要让事务成功WATCH到EXEC之间必须保证该key在全局没有被修改包括你自己也不能改。这一点特别容易在自动化脚本里踩坑因为你以为只是开启监视然后执行事务却不小心在WATCH和MULTI之间插入了一条对该key的写命令。3.3 使用Python复现实务里的乐观锁命令行展示的是最直观的原理但真实业务里不会有人用redis-cli去跑事务。下面我用Python的redis-py演示一个更接近生产场景的例子基于WATCH实现乐观锁解决并发扣减库存的问题。先安装依赖pip install redis然后看这段代码import redis r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def decr_stock_optimistic(stock_key): # 开启WATCH监视库存key r.watch(stock_key) # 读取当前库存 stock int(r.get(stock_key)) if stock 0: r.unwatch() return False # 开启事务并扣减库存 pipe r.pipeline(transactionTrue) pipe.multi() pipe.decr(stock_key) pipe.execute() return True # 初始化库存 r.set(product:1001:stock, 10) for i in range(12): result decr_stock_optimistic(product:1001:stock) print(f第{i1}次扣减结果: {result}) if not result: print(库存不足停止扣减) break这段代码里最关键的地方在watch与pipeline的配合。redis-py的watch会开启一个监视状态然后你在事务执行前读取key的值再在pipeline中执行multi和真正的写命令。如果在这段读取到执行之间有另一个客户端修改了stock_keyEXEC时会检测到版本变化pipe.execute()会抛出WatchError你需要捕获这个异常并重新尝试。实际生产中通常会加一个循环重试机制比如重试三次每次失败后重新watch、重新读取、重新判断。我在本地用两个Python进程同时跑这段脚本制造并发竞争结果非常直观大部分时候能正常扣减某一方会抛WatchError然后策略重新读取、重试最终两个进程的总扣减次数加起来正好等于初始库存10。这就是WATCH事务在无锁情况下保证数据一致性的典型案例。它比Redis分布式锁更轻量适合“读改写”场景中冲突不激烈的情况。3.4 事务与Lua脚本的取舍什么时候别用事务Redis在2.6版本引入Lua脚本后事务的地位变得有些微妙。因为Lua脚本在服务器端执行时同样具备“原子性”——在执行过程中其他客户端的命令不会插入。而且Lua脚本还支持逻辑判断、循环能在一个脚本里完成复杂的业务读改写。相比之下事务只能按固定顺序执行你排好的命令没法根据中间结果做动态判断。打个比方事务是一份写好的菜品清单后厨按单子上菜中间不能尝味道改配方Lua脚本则是把厨师请到后厨现炒现调可以根据锅里的情况临场发挥。面试里常问“事务和Lua有什么区别”其实就是问你能不能分清这两者的边界。我的建议是纯批量写不需要中间值判断的场景用事务已经足够需要读改写、条件判断、循环控制的场景优先考虑Lua脚本。实际项目里很多团队已经很少用原生的Redis事务了因为Lua脚本能够覆盖大部分事务的使用场景而且更灵活。不过WATCH这个命令在面试和特定场景下依然有很高的讨论价值比如实现乐观锁、防止超卖。你在回答“为什么用WATCH而不是分布式锁”时如果能说出“WATCH是乐观锁思路、分布式锁是悲观锁思路冲突少时WATCH开销低、但高并发冲突多时重试成本高此时分布式锁或Lua更合适”就已经比大多数人高出一截了。4. 典型应用场景事务到底解决了哪些实际问题4.1 场景一批量写入时保证命令不被打断最简单的需求一个请求要写多个Redis key比如用户一篇文章的标题、作者、点赞数、发布时间。如果不做处理四个SET命令之间可能被别的客户端命令插入虽然最终效果通常差不多但如果你依赖“这批SET必须作为一个整体对外可见”的约束那就需要事务了。举一个更具体的例子在写缓存同时更新一个计数key。比如每发布一篇文章需要设置文章内容缓存并对一个总文章数计数器执行INCR。如果两个操作中间发生了一次Redis连接断开计数器可能没加内容却写进去了后续统计就会偏少。用事务把两个操作包起来要么执行期间不被中断要么整体入队后一起执行至少从Redis单机视角来看操作是串行的。当然如果你的需求更强——比如“文章发布失败时不要写计数”需要在逻辑层判断发布是否成功那Redis事务就帮不上忙了因为你无法在队列中途做判断。这时候要么在应用层提前判断要么用Lua脚本配合服务端逻辑。事务只保证顺序执行不保证业务条件。4.2 场景二WATCH实现乐观锁与防止超卖防止超卖是电商里最经典的案例。库存一共10件100个用户同时发起购买请求怎么保证不会卖出第11件传统方案是加分布式锁用SETNX或者Redisson的RLock实现悲观锁但加锁本身有性能开销也会增加代码复杂度。更轻量的一种方案是WATCH。每个购买请求按如下流程走WATCH库存keyGET当前库存判断库存是否大于0如果大于0MULTI执行DECREXEC如果EXEC返回nil说明库存被其他请求改过重试如果返回成功说明这次扣减成功库存小于等于0则直接失败这个流程就是前面Python示例的原型。它的优点是不需要显式加锁、没有锁等待适合冲突不太激烈的场景。缺点是如果冲突非常严重重试次数会迅速增加CPU和网络开销都会放大。这个思路在秒杀系统里通常会配合队列削峰把请求放到内存队列或消息队列中后端单线程消费再配合Redis的DECR操作比WATCH重试的方案更稳定。4.3 场景二补分布式事务里Redis事务能扮演什么角色有一类搜索热度很高的词比如“订单与库存分布式事务”、“分布式事务一致性”其中问的是Redis事务在微服务架构中的定位。说实话Redis事务本身解决不了跨服务的分布式事务问题它没有分布式协调能力。但在很多实际项目中Redis事务会和消息队列、本地消息表配合承担“最终一致性”里的一部分职责。举个例子订单服务和库存服务分离订单创建成功后需要扣除库存。你不能把“创建订单”和“扣库存”写在同一个Redis事务里因为它们操作的是两个不同服务的库可能是MySQL也可能是Redis。这时候通常的做法是订单服务先落库并写一条本地消息表记录“需要扣库存”然后异步通知库存服务库存服务拿到消息后用Redis DECR扣减库存如果扣减失败则重试或进入人工介入队列。在这个过程中Redis事务可能只是用于某个服务内部的批量操作比如批量更新多个库存项真正的一致性保障靠的是消息表和重试机制。所以如果你在面试里被问到“Redis事务能搞定分布式事务吗”千万不要说“能”。务实的回答是Redis事务只能保证单个Redis实例内一批命令的串行执行无法保证跨服务的数据一致性在分布式系统里最终一致性通常靠消息队列本地事务补偿机制来解决Redis事务更适合做缓存更新、库存扣减这一类局部操作。4.4 场景四Redis缓存治理与事务的关系搜索热词里出现了很多“redis缓存治理”、“缓存穿透”、“缓存击穿”、“缓存雪崩”的词语。这些和事务的关系不大但确实容易混淆因为很多人会把“缓存更新逻辑”当成“事务场景”。这里帮你理清一条线缓存穿透请求一个不存在的数据缓存和数据库都没有每次请求都要穿透到数据库。解决手段是布隆过滤器或缓存空值。缓存击穿热点key过期瞬间大量请求打到数据库。解决手段是互斥锁或者逻辑过期。缓存雪崩大量key同时过期导致数据库压力骤增。解决手段是过期时间加随机值或者集群高可用。在更新缓存这个环节里事务可以用来保证“更新多个缓存key”的一致性或者用Lua脚本实现“读缓存、回源数据库、写缓存”的组合逻辑。比如常见的缓存重建操作先删除旧缓存再回源数据库然后把新结果写回Redis这三个动作如果用原生命令分开执行中间有请求可能读到旧缓存或者空缓存用Lua脚本包起来就能做成一个整体。所以事务和缓存治理并不是互斥的概念而是相辅相成的关系。5. 常见问题与排查技巧实录5.1 为什么我的WATCH事务总是无缘无故返回nil这是我在实际调试中遇到最多的问题而且往往是“看起来什么都没干”却被检测到修改。几个最常见的原因WATCH和MULTI之间你自己的代码里有隐式修改。比如你在watch后又执行了SET key这个key恰好被监视了于是EXEC失败。排查思路给WATCH这段代码加日志打印出从watch到execute之间所有Redis命令。连接复用导致脏状态。如果你用连接池某个连接上一次事务的EXEC失败了但没有正确处理连接状态下一次复用该连接时WATCH的监视可能被残留。这个在redis-py里不常见因为连接重置会清空状态但一旦出了诡异问题首先要想到连接池。过期时间导致key被删除。WATCH监视的key如果在事务执行前过期Redis会视其为修改过EXEC也会返回nil。这个真的是隐形杀手尤其是你习惯给所有缓存key加TTL时。解决办法也很直接要么在watch前提前判断key是否存在要么捕获WatchError后重试整个读-写过程要么把WATCH改成Lua脚本方式用逻辑判断替代监视机制。5.2 事务里执行了错误命令为什么前缀命令没有回滚这个问题在前面原理部分已经讲过但实操中依然有很多人踩坑。比如MULTISET stock 10LPUSH stock abcstock其实是个字符串这里报类型错误SET name testEXEC三条命令的结果是SET stock 10执行成功LPUSH报错SET name test成功。如果业务方以为事务能回滚那么stock已经被改成了10但name也写进去了数据可能就乱了。如果你确实需要“要么全成功、要么全失败”的强一致性我的建议是不要依赖Redis原生的“伪原子性”而是换一种实现方式。比如把所有会出错的命令用一种不会被类型错误干扰的写法包住或者前置校验所有key的类型又或者直接改用Lua脚本。Lua脚本可以通过pcall捕获错误在脚本内部主动返回错误状态实现逻辑上的“全成功或全失败”。不过Lua脚本出错时Redis也不会自动回滚已经执行的写命令所以严格意义上Redis里很难实现数据库式的事务回滚你需要自己对业务做补偿。5.3 事务内命令执行特别慢哪里出了问题有时候事务里命令不多但EXEC整体耗时很高。可能的原因有这些队列里包含了慢命令比如大量的KEYS、SMEMBERS或者SORT这类复杂度较高的命令。在单线程的Redis里一个慢命令会阻塞后续所有命令包括其他客户端的命令。WATCH监视的key太多Redis需要在EXEC时逐一验证版本key数量多时验证开销也会上升。网络往返时间长尤其是把Redis部署在跨地域的云环境中时QUEUED和EXEC之间多次RTT延迟会被放大。事务的每个入队命令都需要一次网络往返命令一多总耗时就是命令数乘以RTT。这是很多“觉得事务慢”的人没注意到的根本原因。优化思路减少事务里的命令数量用pipeline在客户端合并网络请求避免在事务里执行重命令如果事务规模很大考虑改用Lua脚本减少多轮交互。Lua脚本只需要一次网络请求就能在服务器端完成所有逻辑对性能非常友好。5.4 使用Spring Boot操作Redis事务时容易踩的坑很多人用Spring Data Redis操作Redis事务最容易遇到的问题就是“事务注解与Redis事务的混用”。比如你在方法上加了Transactional同时又执行了Redis操作这时如果MyBatis或JPA的数据源提交失败Redis里的数据却已经写进去了整体一致性非常尴尬。Spring Data Redis提供了RedisTemplate的execute方法配合SessionCallback可以实现事务操作。需要注意的是Spring的Redis事务默认把命令缓存在当前连接中并不会立即发送给Redis直到事务边界提交时才真正发送MULTI/EXEC。这个设计有一定迷惑性因为你在代码里调用redisTemplate.opsForValue().set()并不会立即生效要等到外层事务提交后才会执行。如果你在这个事务方法里又调用RedisTemplate的查询方法可能查不到刚刚写入的数据从而引发逻辑错误。这是很多Java开发者会踩的坑。建议的做法是分布式环境下不要用Spring的Transactional去包Redis操作应该单独使用RedisTemplate的SessionCallback自己控制MULTI和EXEC。这样整个事务的生命周期完全由你掌控不会因为外层数据库事务的莫名联动导致意想不到的行为。5.5 线上Redis为何会报“EXEC without MULTI”错误还有一种常见的异常客户端在未发送MULTI的情况下直接发送EXECRedis会返回“ERR EXEC without MULTI”并直接拒绝执行。这个错误通常出现在连接池复用的场景或者代码中对连接状态的管理混乱。比如你在一个连接上手动执行了MULTI然后忘记EXEC或DISCARD直接把连接还回连接池下一个请求拿到同一连接时可能还残留着事务状态或者你通过某个代理层时事务状态被代理层重置了但客户端仍然在发EXEC于是就会报错。解决思路确保每次MULTI之后无论成功失败都执行EXEC或DISCARD如果使用连接池最好在取连接时做状态校验或者直接使用自动管理事务状态的库。排查时可以开启命令日志看看EXEC之前到底有没有收到过MULTI。如果你是用redis-py这类库可以尝试显式执行reset()来清空连接状态。5.6 面试里最容易被追问的5个Redis事务问题这里整理了几个我在面试中常被问、也常见别人被问的问题附带我的回答思路供你参考。Redis事务能保证原子性吗不能。它能保证批命令串行执行、不被打断但不能保证出错全回滚。所以更严谨的说法是“隔离性”而非“原子性”。Redis事务和MySQL事务有什么区别MySQL事务要满足ACIDRedis事务主要保证执行期间的串行性没有隔离级别也没有回滚机制MySQL支持回滚、隔离级别、并发控制两者设计目标不同。WATCH为什么能防止并发超卖因为它实现了乐观锁。监视库存key如果执行前key被其他客户端改过EXEC会被拒绝超卖请求会不断重试直到库存耗尽。Redis事务与Lua脚本有何异同两者都能保证串行性但Lua脚本支持逻辑判断和循环且只需要一次网络请求事务只能按固定顺序执行预排命令。需要复杂业务判断时优先Lua。为什么Redis官方不打算实现事务回滚因为回滚会增加复杂度和性能开销而大部分命令错误源于编程错误开发阶段就能暴露。Redis追求简单、高性能故意放弃回滚能力。如果你能被问到这一层并且能回答出“WATCH内部版本号、队列入队机制、单线程模型、Lua脚本原子执行”这些关键词面试官基本会认为你对Redis事务有扎实理解。6. 我的实操心得与额外建议6.1 生产代码中尽量少用裸事务多用Lua脚本或管道我做过的项目中纯用Redis事务的代码其实很少。事务最大的问题是不能根据中间结果做决策而实际业务很少只是“闷头写一批key”。用户下单、库存扣减、积分增加每个环节之间往往有依赖关系。用Lua脚本把这些逻辑串起来一次网络请求就能在Redis端完成既节省RTT又能保证执行期间不被插入其他命令。如果你确实要用事务那么尽量配合pipeline来减少网络往返。redis-py的pipeline(transactionTrue)会在客户端直接把MULTI、命令、EXEC打包发送比一条命令一次RTT要高效很多。这一点和编程语言本身无关逻辑是通用的事务命令越多你越需要考虑网络开销。6.2 把WATCH当成“乐观锁”而非“事务”来理解从语义上讲WATCH和分布式锁的定位更接近它解决的是并发下的数据竞争问题。面试官问WATCH时本质是在问你知不知道乐观锁和悲观锁的取舍。我在实际项目里也见过很多把分布式锁当成万能药的遇到并发就SETNX最后锁的超时、重入、失效问题缠身。其实如果并发冲突不严重WATCH事务重试的成本远低于锁管理。但要注意一点WATCH只能作用于单个Redis实例。在集群模式下如果key被分散到不同节点跨节点的WATCH是无能为力的。Redis Cluster模式下如果需要跨key事务通常要求所有key属于同一个哈希槽也就是key要带上相同的哈希标签。这个限制直接决定了你能不能在一个事务里同时操作多个业务key。我在设计缓存key的时候会刻意把需要事务操作的key加上相同的tag这样在Cluster下也能使用事务或Lua脚本。6.3 踩过几次坑之后我总结了一套事务使用检查清单现在每当我写一段涉及Redis事务或Lua脚本的代码都会先自问几个问题这批命令是否必须“打包执行”如果只是批量写入其实pipeline就够了事务还要防呆容易理解偏差。是否需要回滚语义如果需要就考虑在应用层做补偿不要指望Redis。是否存在条件判断如果有Lua脚本是更好的选择。代码里WATCH到EXEC之间有没有可能会修改被监视key的代码如果有一定会导致EXEC失败。连接池是否会复用带有旧事务状态的连接如果有程序里必须始终保证事务以EXEC或DISCARD收尾。Redis集群模式下相关key是否在同一个哈希槽如果不是需要重新设计key命名方案。这套检查单看起来很基础但每一次线上事故排查到最后往往就是其中某一条没满足。6.4 关于面试最后再给你一个忠告Redis事务面试题看似简单但拉开差距的往往不是背答案而是能否把“为什么”讲清楚。比如“为什么不回滚”如果你只说“Redis设计时就放弃回滚”那顶多是记性不错但如果你能说出“回滚需要undo日志、状态快照和复杂的恢复策略这与Redis追求极简和高性能的设计相悖而且大部分错误在开发期就能暴露所以官方选择不做回滚”面试官对你的评价会完全不同。理解底层设计哲学比记住结论重要一百倍。另外在回答分布式事务相关问题时一定要表现出“边界感”。Redis事务不是万能的它甚至不是分布式事务方案。能准确说明Redis事务的优点和局限才说明你真正理解了它。我自己从最早把Redis事务当成MySQL事务用到后来被线上超卖问题打击再到用WATCH和Lua脚本逐步解决问题这个过程里踩过的坑其实都能归结为一句工具本身没有好坏只有是否用对了场景。希望这份实战笔记能帮你少走一些弯路。