文章目录
- Redis的事务
- 一、概述
- 1. 什么是事务
- 2. Redis有没有事务?
- 3. Redis事务解决了什么问题
- 二、Redis事务怎么用
- 1. 三个核心命令
- 2. 基本用法
- 3. 关键特性:执行期间不被打断
- 三、Redis事务的"坑"——没有回滚
- 1. 语法错误 vs 运行时错误
- 2. 为什么Redis不支持回滚?
- 3. Redis vs MySQL:事务模型对比
- 四、乐观锁——WATCH命令
- 1. 什么是WATCH
- 2. 工作流程
- 3. 典型场景:转账
- 4. UNWATCH
- 5. 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) "zhangsan"MULTI之后每个命令返回QUEUED,表示入队了但还没执行。EXEC一把梭,队列里的命令按顺序跑完,返回每个命令的结果。
3. 关键特性:执行期间不被打断
EXEC执行期间,Redis是单线程一口气跑完整个队列的,其他客户端的命令必须排队等着。这是Redis事务能保证"隔离性"的根本原因——跟MySQL靠锁和MVCC完全是不同的思路。
三、Redis事务的"坑"——没有回滚
1. 语法错误 vs 运行时错误
这是最容易踩的坑。Redis对两种错误的处理逻辑完全不同:
| 错误类型 | 发生时机 | 举例 | 事务行为 |
|---|---|---|---|
| 语法错误 | 命令入队时 | SET name(少参数) | 整个事务在EXEC时直接拒绝执行 |
| 运行时错误 | EXEC执行时 | INCR name(name存的是字符串) | 出错的命令失败,其他命令照常执行 |
(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:事务模型对比
| 维度 | Redis | MySQL(InnoDB) |
|---|---|---|
| 原子性 | 部分支持。语法错误→整体回滚,运行时错误→不回滚 | 完整支持。任何错误都回滚整个事务 |
| 隔离性 | 靠单线程串行执行保证,天然无并发问题 | 靠锁+MVCC,有四种隔离级别可选 |
| 持久性 | 取决于持久化策略(RDB/AOF),可能丢数据 | redo log + binlog 双重保障 |
| 一致性 | 需要开发者自己保证 | 约束(主键、外键、唯一索引)兜底 |
| 回滚机制 | 无回滚日志,执行了就执行了 | undo log完整记录,随时回滚 |
一句话总结:MySQL的事务是"重装战士",Redis的事务是"轻装斥候"。各有所长,用对场景就行。
四、乐观锁——WATCH命令
1. 什么是WATCH
WATCH命令用于在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 WATCH | MySQL乐观锁 | |
|---|---|---|
| 实现方式 | WATCH命令监视Key | 版本号字段(version)或时间戳 |
| 冲突检测 | EXEC时自动检测Key是否被改 | UPDATE ... WHERE version = ?,看affected rows |
| 重试 | 业务层手动重试 | 业务层判断affected rows=0后重试 |
| 粒度 | Key级别 | 行级别 |
本质上都是"先干活,提交时验冲突"。区别在于Redis直接内置了这个能力,MySQL需要你自己加version字段。
五、Pipeline vs Transaction
很多人把这俩搞混,其实完全不是一回事。
| Pipeline | Transaction(MULTI/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. 简单示例
-- 原子性地检查库存并扣减localstock=redis.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。
- 最佳实践:别非此即彼,该组合的时候就组合。