Redis 的 WATCH 命令如何实现乐观锁

在 Redis 的面试中,关于事务与并发控制的问题层出不穷,其中“WATCH 命令如何实现乐观锁”是一个高频考点。很多开发者对 Redis 事务的理解停留在 MULTI/EXEC 的层面,却对 WATCH 的机制和它在并发场景下的价值一知半解。本文将从乐观锁的基本概念出发,深入剖析 WATCH 命令的实现原理,并结合实际场景说明如何利用它构建可靠的并发控制逻辑。

一、为什么需要乐观锁?

在并发编程中,多个客户端可能同时读写同一份数据。如果不加控制,就会出现更新丢失、脏读等问题。常见的解决方案有两种:

  • 悲观锁:假设冲突一定会发生,因此在操作数据前先加锁,阻止其他客户端访问。比如数据库中的 SELECT ... FOR UPDATE
  • 乐观锁:假设冲突很少发生,因此不加锁,而是在提交更新时检查数据是否被其他客户端修改过。如果被修改过,则放弃或重试。

Redis 本身是单线程处理命令的,但这并不意味着多个客户端之间不会产生竞态条件。例如,客户端 A 读取了某个 key 的值,准备基于这个值做计算后再写回;与此同时客户端 B 也读取了同一个值并写回。如果两个客户端都直接写回,就会导致其中一个的更新被覆盖。WATCH 命令正是为了解决这类问题而设计的乐观锁机制。

二、WATCH 命令的基本用法

WATCH 命令的语法非常简单:

WATCH key [key ...]

它通常与 MULTI/EXEC 事务配合使用。基本流程如下:

  1. 客户端使用 WATCH 监视一个或多个 key。
  2. 客户端读取被监视 key 的当前值,进行业务计算。
  3. 客户端开启事务 MULTI,将写操作命令入队。
  4. 客户端执行 EXEC 提交事务。

如果在 WATCH 之后、EXEC 之前,任何一个被监视的 key 被其他客户端修改过,那么 EXEC 会执行失败,返回 nil,事务中的所有命令都不会被执行。

一个典型的例子是原子性地递增一个值,但要求只有当值未超过上限时才递增:

WATCH counter
value = GET counter
if value < 100:
    MULTI
    INCR counter
    EXEC
else:
    UNWATCH

如果在这个过程期间有其他客户端修改了 counterEXEC 就会失败,客户端可以选择重试整个流程。

三、WATCH 的实现原理

要理解 WATCH 如何实现乐观锁,需要从 Redis 的内部数据结构说起。

1. 监视的存储:watched_keys 字典

每个 Redis 数据库(redisDb)中都维护了一个 watched_keys 字典。这个字典的键是被监视的 key,值是一个链表,链表中保存了所有监视该 key 的客户端。也就是说,Redis 能够快速地根据一个 key 找到所有正在监视它的客户端。

2. 客户端标志:CLIENT_DIRTY_CAS

每个 Redis 客户端(client)都有一个标志位 CLIENT_DIRTY_CAS。当被监视的 key 被修改时,Redis 会将该 key 对应的所有客户端的这个标志位置为 1,表示“客户端的 CAS(Check-And-Set)条件已被破坏”。

3. 修改触发:touchWatchedKey 函数

每当一个 key 被修改(包括被删除、过期等),Redis 都会调用 touchWatchedKey 函数。这个函数会查找 watched_keys 字典,找到所有监视该 key 的客户端,并将它们的 CLIENT_DIRTY_CAS 标志置位。注意,这里并不关心修改操作是否在事务中,也不关心修改后的值是什么,只要 key 被触碰过,就视为已修改。

4. EXEC 时的检查

当客户端执行 EXEC 时,Redis 会检查该客户端的 CLIENT_DIRTY_CAS 标志:

  • 如果标志为 1,说明至少有一个被监视的 key 在 WATCH 之后被修改过,此时 Redis 会放弃执行事务,返回 nil
  • 如果标志为 0,说明所有被监视的 key 都没有被修改过,Redis 会依次执行事务队列中的所有命令。

5. UNWATCH 与 EXEC 后的清理

无论事务是否执行成功,EXEC 执行后都会清除客户端所有的监视信息,包括从 watched_keys 字典中移除该客户端,并重置 CLIENT_DIRTY_CAS 标志。此外,客户端也可以主动调用 UNWATCH 来取消所有监视。

四、WATCH 的乐观锁特性分析

从上述实现可以看出,WATCH 提供的乐观锁具有以下特点:

  • 非阻塞:WATCH 本身不会阻塞任何操作,其他客户端可以自由修改被监视的 key,只是修改会导致监视者的 CAS 失败。
  • 一次性:WATCH 的监视效果只在当前连接的下一次 EXEC 之前有效。一旦 EXEC 执行(无论成功与否),所有监视都会被清除。
  • 粗粒度:只要 key 被修改过,无论修改成什么值,都会导致 CAS 失败。即使其他客户端把值改成了相同的值,也会被视为冲突。
  • 不保证原子性:WATCH + MULTI + EXEC 本身并不保证“读取-计算-写入”的原子性,它只是保证在 EXEC 时如果数据被修改过就放弃执行。因此,客户端通常需要配合重试逻辑。

五、常见误区与面试要点

在面试中,关于 WATCH 的常见问题包括:

误区一:WATCH 可以替代分布式锁。
WATCH 只能用于乐观锁场景,它不提供互斥性。多个客户端可以同时读取同一个 key,只有在提交时才会检查冲突。如果需要强互斥,应该使用 SETNX 或 Redlock 等分布式锁方案。

误区二:WATCH 监视的 key 过期也会触发 CAS 失败。
这是正确的。key 过期本质上是一种删除操作,会触发 touchWatchedKey,因此也会导致 CAS 失败。这一点在面试中经常被问到。

误区三:EXEC 失败后事务中的命令部分执行。
这是错误的。EXEC 失败时,事务队列中的所有命令都不会被执行,Redis 保证要么全部执行,要么全部不执行。

误区四:WATCH 可以在 MULTI 之后使用。
这是错误的。WATCH 必须在 MULTI 之前执行。如果在 MULTI 之后执行 WATCH,Redis 会返回错误。

六、实战建议

在实际开发中,使用 WATCH 实现乐观锁时,建议遵循以下模式:

  1. 使用 WATCH 监视关键 key。
  2. 读取 key 的当前值并进行业务判断。
  3. 如果条件满足,开启 MULTI,将写命令入队,然后 EXEC
  4. 如果 EXEC 返回 nil,说明发生冲突,可以选择重试(通常配合循环和最大重试次数)。
  5. 如果条件不满足,调用 UNWATCH 释放监视,避免不必要的冲突检测。

需要注意的是,WATCH 的重试逻辑应该在应用层实现,并且要控制重试次数,避免在高并发场景下出现活锁。

七、总结

WATCH 命令是 Redis 实现乐观锁的核心机制。它通过 watched_keys 字典和 CLIENT_DIRTY_CAS 标志,在不阻塞其他客户端的前提下,检测被监视 key 是否在事务提交前被修改。如果发生修改,EXEC 会放弃执行事务,从而避免更新丢失。理解 WATCH 的实现原理,不仅有助于回答面试题,更能帮助我们在实际项目中正确使用 Redis 事务,构建可靠的并发控制逻辑。

未经允许不得转载:任鹏个人博客 » Redis 的 WATCH 命令如何实现乐观锁

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏