Redis事务
引言
我们通常理解的"事务",简单来说就是一句话:要么一起成功,要么一起失败,绝不能出现"只完成一半"的情况。
这就好比一场资金转账。假设用户A要给用户B转账5块钱。在程序里,这通常包含两个动作:
- 扣款:从用户A的账户里扣除5块钱。
- 入账:给用户B的账户增加5块钱。
在现实世界里,这两个动作必须是铁板一块的。如果扣款成功了,但入账失败了(比如网络突然断了),那这5块钱就凭空消失了,这在金融系统里是绝对无法容忍的灾难。反之,如果扣款失败了,入账却成功了,那银行就亏大了。 所以,我们需要一种机制,把这两个动作打包成一个"不可分割"的整体。这就是原子性(Atomicity)。
Redis执行事务的命令
第一步:WATCH
行动开始前,我们需要确保目标数据是"安全"的。WATCH 命令就是我们的"盯梢员"。
在执行事务之前,我们先用 WATCH key [key ...] 监视那些关键的键(比如用户的余额)。这就好比在转账前,你死死盯着账户余额的数字。如果在行动结束前,这个数字被其他人改动了,那我们的行动就会立刻中止,以此来避免基于过期数据做出错误的决策。
第二步:MULTI
一旦确认环境"安全",我们就可以开始收集指令了。MULTI 命令标志着事务的正式开始。
当你输入 MULTI 后,Redis会进入"排队模式"。接下来你输入的所有命令(比如扣款、入账),都不会立即执行,而是被放入一个队列中静静等待。这一步确保了我们的操作序列是完整的,不会被中间插队。
第三步:EXEC
当所有命令都准备就绪,就是发起总攻的时刻。EXEC 命令会触发队列中所有命令的执行。
Redis会一口气、按顺序地把队列里的命令全部执行完。更重要的是,它会先回头检查一下 WATCH 盯着的那些键:如果它们没变,命令就顺利执行;如果它们变了,Redis会直接放弃整个队列的操作(返回nil),绝不允许出现"半成品"的脏数据。
补充动作:解散与重置
DISCARD:如果你在MULTI之后突然不想干了,可以用DISCARD来清空队列,让Redis恢复到正常状态。UNWATCH:如果你不想再监视某些键了,或者事务执行完毕(EXEC或DISCARD后会自动取消监视),可以用UNWATCH来取消监视,释放资源。
"伪"原子性
在 MySQL 等传统关系型数据库中,原子性意味着"全有或全无"(All or Nothing)。如果中间出错,系统会自动执行回滚(Rollback),把数据恢复到最初的状态。
然而,Redis 事务的哲学截然不同。它奉行"简单至上 ",不支持传统意义上的回滚机制。这主要体现在两个方面:
-
不回滚业务错误
如果事务中的某条命令执行失败(比如对一个字符串类型的键执行 INCR),Redis 不会撤销之前已经执行成功的命令。例如,A扣款成功了,但给B入账时命令写错了,Redis不会把A的钱还回去。它只会记录这个错误并继续执行后续命令(如果有的话)。
-
靠"放弃"来保证一致性
既然不支持回滚,那怎么保证数据不出错呢?答案就是 WATCH。
Redis 的策略是:与其出错后费力地回滚,不如在出错前直接放弃。
通过 WATCH 机制,Redis 能够检测到数据是否被并发修改。如果检测到冲突,它不会去尝试修正数据,而是直接放弃整个事务(返回 nil)。这就把处理问题的责任推给了客户端------客户端收到失败信号后,应该重新尝试整个操作。
为什么没有回滚?
你可能会问,为什么不实现像MySQL那样的回滚机制呢?官方的解释是:Redis追求简单和极致的性能。回滚机制需要记录undo log(回滚日志),这会带来额外的开销和复杂性。对于Redis来说,大多数命令都是简单的写操作,且数据通常作为缓存存在,即使出错,也可以通过从后端数据库重新加载来恢复。因此,Redis选择了一种更轻量级的方案。
与Lua脚本的抉择
Redis还支持通过EVAL命令执行Lua脚本。Lua脚本在Redis中也是原子执行的,并且它支持真正的条件判断和错误处理。在很多场景下,Lua脚本可以替代Redis事务,甚至比事务更强大、更简洁。
那么,何时使用事务,何时使用Lua脚本呢?
- 使用Redis事务:当你需要执行的是一组简单的、固定的Redis命令,且业务逻辑相对简单时。
- 使用Lua脚本:当你需要复杂的逻辑判断、循环、或者需要在服务端进行复杂的计算时。Lua脚本将逻辑封装在服务端,减少了网络往返,性能通常更好。