当前位置:首页 > 问答 > 正文

Redis真没事务吗?难道就没有啥办法解决这个问题?

Redis真没事务吗?难道就没有啥办法解决这个问题?”这个话题,首先需要澄清一个核心的误解:Redis是有事务功能的,这个说法可能源于对数据库“事务”概念的不同理解,我们通常理解的事务,比如在MySQL这类关系型数据库里,要满足ACID原则,其中的“C”代表一致性(Consistency),“I”代表隔离性(Isolation),这意味着事务中的一系列操作要么全部成功,要么全部失败,并且在执行过程中,不会被其他事务干扰。

Redis确实提供了事务机制,但它的实现方式和传统关系型数据库有本质区别,我们不能用MySQL的标准来生搬硬套Redis,下面就来详细说说Redis的事务是怎么回事,以及当它不够用时,还有什么别的招儿。

Redis的“事务”是什么?

Redis的事务主要通过三个命令来实现:MULTI, EXEC, DISCARD

  • MULTI:你可以把它想象成一个“开始组队”的命令,输入MULTI后,Redis并不会立即执行你后续输入的命令,而是把这些命令一个一个地排进一个队列里。
  • 你继续输入你的操作命令,set key1 value1set key2 value2sadd myset member1 等等,这些命令都会排队等候。
  • EXEC:这个命令就是“执行队伍”的口令,当你输入EXEC,Redis会一口气、按顺序地执行队列里所有的命令。
  • DISCARD:如果你想取消这次“组队”,不执行队列里的命令了,就用DISCARD来解散队伍,清空队列。

这算真正的事务吗? 它实现了“原子性”的一部分:通过EXEC命令,所有操作被一起执行,它有一个非常重要的特性,也是容易被误解的点:Redis事务不支持回滚(Rollback)

这意味着什么呢?假设你在MULTI后输入了10个命令,当EXEC执行时,前5个命令都成功了,但第6个命令因为语法错误(比如命令写错了)根本执行不了,或者更常见的,因为数据类型错误(比如对一个字符串类型的值执行列表的LPOP操作)而失败,Redis不会自动把前5个成功的操作撤销掉,它会继续尝试执行第7、8、9、10个命令。

Redis真没事务吗?难道就没有啥办法解决这个问题?

这和MySQL那种一旦出错就全部回滚的机制完全不同,Redis官方对此的解释是:Redis命令失败通常只会在语法错误时发生(这应该在开发测试阶段就发现),而类型错误这类问题属于编程错误,同样应该在开发阶段解决,支持回滚会让Redis的设计变得复杂,并且影响性能,它选择了这种更简单、更快速的“半原子性”方式。

Redis事务的局限性(也就是“问题”所在)

除了不支持回滚,Redis事务还有一个更关键的局限性:它不具备完整的隔离性

举个例子,经典的银行转账问题:账户A有100元,要转50元给账户B,在MULTI之后,我们执行两个命令:decrby A 50incrby B 50,在传统数据库里,事务执行过程中,其他事务是看不到它中间状态的,但在Redis里,情况不一样。

假设在A账户刚扣完50元,但B账户还没加上50元的这个“瞬间”(实际上是在EXEC执行前的排队期间,这个例子更适用于后面讲的WATCH机制),另一个客户端查询A和B的总额,它会看到总额变成了50元(A=50,B=0),这显然是不对的,因为Redis的事务命令只是排队,并没有像MySQL那样产生隔离级别。

Redis真没事务吗?难道就没有啥办法解决这个问题?

解决更复杂场景的“大招”:乐观锁(WATCH)

为了解决上面提到的隔离性问题,Redis提供了一个非常巧妙的机制:WATCH命令,这其实就是一种“乐观锁”。

它的工作方式很像网上购物:

  1. 监视商品:在开启事务(MULTI)之前,你先用WATCH命令盯住一个或多个关键的键(key),比如上面例子中的账户A和账户B的键,这就好比你把心仪的商品加入了购物车。
  2. 检查库存:在MULTIEXEC之间,你进行你的操作编排(扣款、加款)。
  3. 下单付款:当你按下EXEC“提交订单”时,Redis会做一个重要的检查:在你WATCH之后,有没有其他客户端修改过你盯着的这些键?如果没有任何人动过(商品库存没变),那么恭喜,你的交易(EXEC)成功执行,但如果这期间有别的客户端修改了A或B的余额(比如另一个转账操作抢先完成了),Redis会发现你“监视”的数据已经被动了手脚,那么它会毫不犹豫地让你的整个事务失败,EXEC会返回空值,表示执行失败。

这样一来,通过WATCH,我们就实现了类似隔离性的效果,虽然事务本身不支持回滚,但通过“乐观锁”机制,我们在事务执行前就避免了数据竞争导致的不一致,如果事务因为WATCH失败,应用程序的逻辑通常需要重试这个事务,直到成功为止。

另一种思路:Lua脚本

Redis真没事务吗?难道就没有啥办法解决这个问题?

除了MULTI/EXECWATCH,Redis还有一个“终极武器”:Lua脚本

你可以把一系列复杂的Redis操作写成一个Lua脚本,然后一次性发送给Redis服务器执行,Lua脚本在执行时是原子性的,这意味着脚本在执行过程中,不会被任何其他命令打断,其他客户端的所有命令都要等这个脚本执行完才能执行,它具备了真正的原子性,也自然解决了隔离性问题。

对于复杂的业务逻辑,比如需要先判断某个条件再执行后续操作的情况,使用Lua脚本要比MULTI/WATCH的组合更简单、更高效,因为WATCH失败需要重试,而Lua脚本一次搞定,在大多数需要强原子性保证的复杂场景下,Lua脚本是更受推荐的方式。

回到最初的问题:“Redis真没事务吗?难道就没有啥办法解决这个问题?”

  • Redis有事务,但它是另一种形态的事务,特点是批量执行但不支持自动回滚。
  • 问题确实存在,主要体现在隔离性不足和无法回滚。
  • 解决办法很明确
    • 对于简单的批量操作,不介意中间错误继续执行的,直接用MULTI/EXEC
    • 对于需要避免数据竞争的场景(如转账),使用WATCH+MULTI/EXEC实现的乐观锁。
    • 对于复杂的、需要强原子性保证的业务逻辑,首选Lua脚本

不能说Redis没有事务,而是要根据你的具体业务场景,选择最适合的“工具”来解决问题。