文章目录Redis的事务一、概述1. 什么是事务2. Redis有没有事务3. Redis事务解决了什么问题二、Redis事务怎么用1. 三个核心命令2. 基本用法3. 关键特性执行期间不被打断三、Redis事务的坑——没有回滚1. 语法错误 vs 运行时错误2. 为什么Redis不支持回滚3. Redis vs MySQL事务模型对比四、乐观锁——WATCH命令1. 什么是WATCH2. 工作流程3. 典型场景转账4. UNWATCH5. WATCH vs MySQL的乐观锁五、Pipeline vs Transaction六、Lua脚本——另一种事务1. Lua脚本为什么能当事务用2. Lua比原生事务好在哪3. 简单示例4. 三种方案选型建议七、Redis事务 vs MySQL事务场景选择1. 什么时候用Redis事务2. 什么时候必须用MySQL事务3. 不是二选一而是组合用八、小结Redis的事务一、概述1. 什么是事务把一组操作打包成一个执行单元要么全执行要么全不执行保证数据一致性。2. Redis有没有事务有但跟你熟悉的MySQL事务完全是两码事。Redis提供了MULTI/EXEC/WATCH这一套机制来实现事务不过它走的是轻量级路线——够用但别指望它能像关系型数据库那样面面俱到。3. Redis事务解决了什么问题保证一批命令按顺序、不被打断地执行完中途不会插入别的客户端的命令。配合WATCH还能实现乐观锁解决并发修改的冲突问题。二、Redis事务怎么用1. 三个核心命令Redis事务由 MULTI、EXEC、DISCARD 三个命令配合完成。MULTI 标记事务开始EXEC 触发执行DISCARD 中途放弃。MULTI开启事务之后的命令不会立即执行而是排进一个队列。EXEC执行事务队列中的所有命令。DISCARD清空队列放弃本次事务。2. 基本用法127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET name zhangsan QUEUED 127.0.0.1:6379 INCR score QUEUED 127.0.0.1:6379 GET name QUEUED 127.0.0.1:6379 EXEC 1) OK 2) (integer) 1 3) zhangsanMULTI之后每个命令返回QUEUED表示入队了但还没执行。EXEC一把梭队列里的命令按顺序跑完返回每个命令的结果。3. 关键特性执行期间不被打断EXEC执行期间Redis是单线程一口气跑完整个队列的其他客户端的命令必须排队等着。这是Redis事务能保证隔离性的根本原因——跟MySQL靠锁和MVCC完全是不同的思路。三、Redis事务的坑——没有回滚1. 语法错误 vs 运行时错误这是最容易踩的坑。Redis对两种错误的处理逻辑完全不同错误类型发生时机举例事务行为语法错误命令入队时SET name少参数整个事务在EXEC时直接拒绝执行运行时错误EXEC执行时INCR namename存的是字符串出错的命令失败其他命令照常执行1语法错误——整批回滚127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET name zhangsan QUEUED 127.0.0.1:6379 SET name # 参数不对 (error) ERR wrong number of arguments 127.0.0.1:6379 EXEC (error) EXECABORT Transaction discarded because of previous errors.入队时就报错了EXEC直接拒绝整个事务第一条SET也不会执行。2运行时错误——只炸一条127.0.0.1:6379 MULTI OK 127.0.0.1:6379 SET name zhangsan QUEUED 127.0.0.1:6379 INCR name # 对字符串执行INCR但入队时不检查类型 QUEUED 127.0.0.1:6379 SET age 18 QUEUED 127.0.0.1:6379 EXEC 1) OK 2) (error) ERR value is not an integer or out of range 3) OK注意第1条和第3条都执行成功了只有第2条挂了。这就是Redis的设计哲学——命令入队时报错不掉的执行时报错了也不影响其他命令。2. 为什么Redis不支持回滚官方的说法很直白不支持回滚不是因为实现不了而是因为没必要。Redis的命令设计本身就是简洁、类型明确的运行时错误通常只来自程序逻辑错误比如对String执行LPOP这种情况应该在开发阶段就发现并干掉而不是靠线上回滚来兜底。不做回滚换来了更简单、更快的内部实现。3. Redis vs MySQL事务模型对比维度RedisMySQLInnoDB原子性部分支持。语法错误→整体回滚运行时错误→不回滚完整支持。任何错误都回滚整个事务隔离性靠单线程串行执行保证天然无并发问题靠锁MVCC有四种隔离级别可选持久性取决于持久化策略RDB/AOF可能丢数据redo log binlog 双重保障一致性需要开发者自己保证约束主键、外键、唯一索引兜底回滚机制无回滚日志执行了就执行了undo log完整记录随时回滚一句话总结MySQL的事务是重装战士Redis的事务是轻装斥候。各有所长用对场景就行。四、乐观锁——WATCH命令1. 什么是WATCHWATCH命令用于在MULTI之前监视一个或多个Key。如果EXEC执行前这些Key被其他客户端修改过整个事务就会被拒绝执行。这就实现了乐观锁Optimistic Locking——假设没人跟我抢先去干活提交的时候检查一下发现有人动了就放弃重来。2. 工作流程客户端A: 客户端B: WATCH money GET money → 100 MULTI SET money 80 QUEUED SET money 200 # B在A的EXEC之前改了money OK EXEC → (nil) # 事务被拒绝A监视了money在MULTI到EXEC之间B把money改了。EXEC返回nil事务没有执行。3. 典型场景转账# 给zhangsan减100给lisi加100 WATCH zhangsan_money lisi_money MULTI DECRBY zhangsan_money 100 INCRBY lisi_money 100 EXEC如果EXEC返回nil说明数据在期间被动了通常的做法是重试。4. UNWATCH执行EXEC无论成功还是被拒绝或者DISCARD之后WATCH自动解除。也可以手动UNWATCH取消对所有Key的监视。5. WATCH vs MySQL的乐观锁原理是一样的实现方式不同Redis WATCHMySQL乐观锁实现方式WATCH命令监视Key版本号字段version或时间戳冲突检测EXEC时自动检测Key是否被改UPDATE ... WHERE version ?看affected rows重试业务层手动重试业务层判断affected rows0后重试粒度Key级别行级别本质上都是先干活提交时验冲突。区别在于Redis直接内置了这个能力MySQL需要你自己加version字段。五、Pipeline vs Transaction很多人把这俩搞混其实完全不是一回事。PipelineTransactionMULTI/EXEC目的减少RTT网络往返提升吞吐保证原子性命令是否排队否命令到服务端后立即执行是MULTI后命令只入队不执行中间结果可见每条命令执行后立即可见EXEC前不可见能否穿插其他客户端命令可以单条命令级别EXEC期间不可以事务级别典型用途批量插入数据转账、抢购等需要原子性的场景可以组合用吗当然。先MULTI把一批命令通过Pipeline发过去最后EXEC。兼顾性能和原子性。六、Lua脚本——另一种事务到了这必须提一下Lua因为实际开发中很多人更倾向用Lua而不是MULTI/EXEC。1. Lua脚本为什么能当事务用Redis执行Lua脚本时整个脚本作为一个整体被原子执行。脚本执行期间其他客户端的命令必须等待跟EXEC的效果一样。2. Lua比原生事务好在哪能做逻辑判断事务只能串命令Lua可以if/else、循环、读数据后做判断。减少网络开销一段复杂逻辑一次发过去不用多次RTT。真正的原子性脚本里某条命令失败可以根据逻辑决定是否继续比原生事务运行时错误不管的机制灵活得多。3. 简单示例-- 原子性地检查库存并扣减localstockredis.call(GET,KEYS[1])iftonumber(stock)0thenredis.call(DECR,KEYS[1])return1elsereturn0end这种逻辑用MULTI/EXEC做不到——因为事务里没法先读数据、再决定下一步做什么。这也是多数抢购、限流场景选Lua的原因。4. 三种方案选型建议场景推荐方案简单批量写入不需要逻辑判断Pipeline需要原子性但逻辑简单、不需要读后判断MULTI/EXEC WATCH需要原子性 读后判断 复杂逻辑Lua脚本需要跨Key强事务一致性别用Redis了上MySQL吧七、Redis事务 vs MySQL事务场景选择学技术最容易犯的错就是把一个东西用到所有地方。Redis和MySQL的事务各有自己的主战场1. 什么时候用Redis事务对性能要求极高Redis的事务几乎没有额外开销因为你只是把命令攒起来一次性执行。需要原子性但不要持久化保证比如计数、排行榜、Session管理。并发冲突低WATCH的乐观锁在冲突少的场景下效率极高不像MySQL行锁可能引发死锁。抢购、秒杀Lua脚本Redis原子性扛住高并发。2. 什么时候必须用MySQL事务需要ACID完整保障比如订单、支付、账户余额少一个特性都可能出生产事故。涉及多表关联操作Redis不是关系型没有外键、没有联表。需要复杂查询 事务查完再决定滚不滚Redis的事务做不到。需要事后审计MySQL的binlog可以做数据回溯、审计Redis的AOF主要用来恢复。3. 不是二选一而是组合用实际项目里Redis和MySQL经常是配合关系而不是替代关系用Redis扛读流量、做缓存、处理高并发扣减。用MySQL落盘存储、保证最终一致性、做复杂查询。Redis里扣完库存异步发消息给MySQL落盘——最终一致性架构。八、小结Redis有事务但不等于MySQL那种ACID事务——它只保证命令的原子执行不保证数据完整性也不支持回滚。MULTI/EXEC/DISCARD是基本组合WATCH用来实现乐观锁。语法错误会导致整个事务拒绝执行运行时错误只影响出错的那条命令其他照常执行——这是跟MySQL最大的行为差异。Pipeline解决的是网络开销问题事务解决的是原子性问题两者可以组合。Lua脚本是更灵活、更强大的事务方案实际开发中往往是首选。选Redis事务还是MySQL事务看场景高性能、低冲突、逻辑简单→Redis强一致性、复杂关联、需要回滚→MySQL。最佳实践别非此即彼该组合的时候就组合。