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

Redis的事务锁真是并发控制里的秘密武器,聊聊它到底怎么帮忙防止冲突和乱序

关于Redis在并发控制中的作用,尤其是在处理多个客户端同时想修改同一份数据时,它提供的“事务锁”机制确实是一个非常巧妙且强大的工具,很多人可能听说过数据库的事务,但Redis的事务,特别是结合了WATCH命令的乐观锁,其工作方式更像一个机智的协调员,而不是一个强硬的保安,它能有效防止冲突和乱序,确保数据的一致性。

要理解问题所在:冲突与乱序是怎么产生的?

想象一个简单的场景:一个电商平台,某件热门商品只剩最后一件库存,用户A和用户B几乎在同一毫秒点击了“立即购买”,如果没有控制机制,可能会发生这样的情况:

  1. 系统A读取库存,发现是1。
  2. 几乎同时,系统B也读取库存,发现也是1。
  3. 系统A计算:1 - 1 = 0,于是准备将库存更新为0,并生成订单。
  4. 系统B也计算:1 - 1 = 0,也准备将库存更新为0,并生成订单。
  5. 库存变成了0,但却产生了两个订单,这就是典型的“超卖”问题,根源在于读取和写入这两个操作不是原子性的,中间被其他操作插入了,导致了数据冲突和逻辑上的乱序。

Redis的普通事务(MULTI/EXEC)能解决吗?

Redis确实有MULTI和EXEC命令,可以把一系列命令打包成一个队列,然后一次性、按顺序地执行,这确实保证了在EXEC命令执行时,队列中的命令不会被其他客户端的命令打断,解决了单个命令执行过程中的乱序问题。

它解决不了我们上面提到的库存问题,因为MULTI只是把命令排队,在MULTI之后、EXEC之前的这个准备阶段,其他客户端仍然可以修改库存的值,在A客户端执行了MULTI,然后读取库存(假设通过GET命令,但这命令已经在队列里了,实际值还没变),但还没EXEC时,B客户端可能已经飞快地完成了“读库存、减库存、写回”的整个操作,这时A再EXEC,它操作的已经是过时的库存数据了,冲突依然发生。

Redis的事务锁真是并发控制里的秘密武器,聊聊它到底怎么帮忙防止冲突和乱序

秘密武器登场:WATCH命令实现的乐观锁

Redis真正的法宝是WATCH命令,它提供了一种“乐观锁”的机制,乐观锁的核心思想是:我相信在我修改数据之前,很大概率别人不会来改它,但如果真的被改了,我就放弃这次操作,重头再来。

这个过程就像网上抢票:你选好座位点击支付,系统会告诉你“请在三分钟内完成支付”,这三分钟里,这个座位在逻辑上是为你保留的(被WATCH了),但如果另一个技术更高超的人,用更快的网络和手速,在你输入密码完成支付前,抢先一步买走了这个票(修改了数据),你的支付页面就会提示“票已售罄,请重新选择”,这就是乐观锁——不阻止别人尝试,但通过验证版本(或数据值)来保证最终只有一个人能成功。

Redis的事务锁真是并发控制里的秘密武器,聊聊它到底怎么帮忙防止冲突和乱序

具体到Redis的操作流程是这样的:

  1. 监视(WATCH):在开启事务之前,客户端先用WATCH命令盯住一个或多个关键的键(Key),比如我们的product:1001:stock(商品库存键),从这一刻起,Redis服务器会记下这个键当前的值版本(可以简单理解为一个标记)。
  2. 开启事务与执行操作:然后客户端照常使用MULTI开启事务,并放入一系列命令,比如DECR(减1)命令来扣减库存。
  3. 执行与校验(EXEC):当客户端发出EXEC命令准备执行事务时,秘密检查就在这里发生了,Redis不会立刻执行队列里的命令,而是会先去检查所有被WATCH的键,从WATCH之后到现在,有没有被其他客户端修改过。
    • 如果没有任何一个被WATCH的键被修改过:太好了,说明风平浪静,Redis会正常地、原子性地执行事务队列中的所有命令,事务成功。
    • 如果任何一个被WATCH的键被修改了:糟了,有“第三者”插足,Redis会认为这次事务执行的基础已经失效(就像你看到座位被别人抢了),于是直接放弃执行整个事务队列,返回一个空值(nil)给客户端,表示事务执行失败。

这如何防止了冲突和乱序?

回到我们的库存例子,现在用WATCH机制来演一遍:

  1. 客户端A:WATCH product:1001:stock,盯住库存。
  2. 客户端A:读取库存,值是1。
  3. 客户端B:它也WATCH了同一个库存键,读取库存也是1。
  4. 客户端A:MULTI,然后发送DECR product:1001:stock命令(扣减库存)。
  5. 客户端B:也MULTI,然后发送DECR product:1001:stock命令。
  6. 客户端A:EXEC,Redis检查发现,自从A WATCH这个键以来,它的值没有被改动过,于是成功执行,库存减为0,事务成功。
  7. 客户端BEXEC,Redis检查发现,这个键的值已经被A改动了(从1变成了0)!于是B的事务被无情驳回,返回失败,B的客户端逻辑可以收到这个失败信号,然后选择重试(重新WATCH、读取、判断、再操作)或者直接告诉用户“库存不足”。

通过这个机制,即使多个客户端的操作在时间上是乱序交错的,WATCH命令就像给数据贴了一个“易碎贴”,任何提前的触碰都会在最后结算(EXEC)时被发现,从而确保只有第一个完成修改的事务能成功,后续的冲突事务都会失败,这就完美地防止了数据冲突,并保证了操作逻辑的正确顺序。

这种机制要求客户端在事务失败后要有重试逻辑,它不适合竞争极端激烈的场景(否则会大量重试),但对于绝大多数常见的并发控制场景,Redis的这套“事务锁”无疑是简单、高效且强大的秘密武器。 基于Redis官方文档中关于Transactions和WATCH的说明,以及常见的并发编程模式。)