Redis Watch 命令详解:原子操作背后的“监视器”
在分布式系统中,多个客户端同时操作同一个数据,很容易引发“脏读”或“数据不一致”问题。就像几个人同时修改一份共享文档,谁先保存谁的版本就生效,容易造成覆盖。Redis 为了解决这类问题,提供了 Redis Watch 命令,它是一种实现乐观锁机制的核心工具。
你可能已经用过 SET、GET、INCR 这些基础命令,但当你需要在复杂场景下确保“操作前数据未被别人修改”,就该引入 WATCH 了。它就像一个“监视器”,在事务开始前盯住某个键,一旦该键被其他客户端修改,当前事务就会自动放弃执行。
Redis Watch 命令的基本原理
WATCH 命令的作用是“监视”一个或多个键。当客户端执行 WATCH 后,Redis 会记录这些键的当前状态。随后如果客户端使用 MULTI 开启事务,Redis 会在执行 EXEC 时检查这些键是否被其他客户端修改过。如果被改过,整个事务就失败,返回 nil,表示“这次操作不执行”。
这背后是典型的乐观锁思想:先假设不会冲突,只有在提交时才检查是否真的冲突。
举个生活中的例子:你去图书馆借一本书,发现它被别人占着。你没有立刻去等,而是告诉管理员“我先记下这本书的编号,等它回来我就马上借”。这就是“监视”——你并不阻塞,只是在等,一旦书被还回去,你就立刻去借。这比直接“占座”更高效。
如何使用 Redis Watch 命令?
基本语法与执行流程
WATCH key1 [key2 ...]
MULTI
EXEC
关键点在于:WATCH 必须在 MULTI 之前执行。如果 WATCH 之后,键被其他客户端修改,EXEC 会返回 nil,表示事务失败。
实际案例:模拟账户转账
假设我们有两个账户:account:alice 和 account:bob,初始余额均为 100 元。我们要实现一个“转账 50 元”的原子操作。
步骤一:初始化数据
SET account:alice 100
SET account:bob 100
步骤二:开启事务并监视账户
WATCH account:alice account:bob
MULTI
DECRBY account:alice 50
INCRBY account:bob 50
EXEC
如果在 EXEC 执行前,没有其他客户端修改 account:alice 或 account:bob,那么事务成功执行,返回结果为数组,包含两个命令的返回值。
步骤三:事务失败的场景
现在我们模拟一个并发冲突:在 WATCH 之后、EXEC 之前,另一个客户端修改了 account:alice。
SET account:alice 90
此时客户端 A 再执行 EXEC,Redis 会检测到 account:alice 被修改过,于是返回:
(nil)
表示事务失败。客户端 A 需要重新执行整个流程,包括重新 WATCH、MULTI、EXEC。
这就是
Redis Watch 命令的“自我保护”机制:它不阻止别人修改,但会提醒你“你监视的资源变了,别再操作了”。
Watch 命令的注意事项
Watch 是“乐观”的,不是“悲观”的
与数据库中的行锁(悲观锁)不同,WATCH 不会阻止其他客户端访问键。它只是“观察”,在提交时才检查。因此性能更高,适合高并发读多写少的场景。
Watch 的作用范围是客户端会话
每个客户端独立地 WATCH 键。如果客户端 A 监视了 key1,客户端 B 也监视了 key1,它们互不影响。每个客户端的事务只关心自己监视的键是否被外部修改。
Watch 会自动取消
一旦 EXEC 成功执行,或者事务被 DISCARD 取消,Redis 会自动释放该客户端所有 WATCH 的键。也就是说,你不需要手动 UNWATCH,除非你想提前放弃监视。
但如果你在
WATCH之后执行了DISCARD,那WATCH会自动失效。这是设计上的安全机制。
Watch 可以监视多个键
你可以在一条命令中监视多个键,比如:
WATCH user:1001 user:1002 user:1003
这在处理关联数据时非常有用,比如用户信息和订单信息同时变更时。
Watch 与事务:配合使用的完整流程
我们来梳理一个完整的事务流程,包括 WATCH、MULTI、EXEC、DISCARD。
WATCH stock:iphone stock:macbook
MULTI
DECR stock:iphone
INCR stock:macbook
EXEC
如果 EXEC 返回一个数组(如 ["1", "2"]),说明成功。如果返回 (nil),说明有冲突,必须重试。
常见错误与解决方案
错误一:WATCH 在 MULTI 之后执行
MULTI
WATCH key1 key2 # ❌ 错误:WATCH 必须在 MULTI 之前
Redis 会报错:WATCH inside MULTI is not allowed。
✅ 正确做法:
WATCH key1 key2
MULTI
EXEC
错误二:在事务中执行了非事务命令
在 MULTI 和 EXEC 之间,只能执行 SET、GET、INCR 等命令。不能执行 WATCH、UNWATCH、DISCARD 等控制命令。
✅ 正确顺序:
WATCH key1
MULTI
SET key1 "value"
EXEC
错误三:忽略事务失败,不重试
当 EXEC 返回 nil,说明事务失败。很多初学者会忽略这个结果,直接继续执行后续逻辑,导致数据不一致。
✅ 正确做法:在代码中检测 EXEC 的返回值,如果是 nil,就重新执行整个流程,直到成功。
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
while True:
try:
r.watch('account:alice', 'account:bob')
# 获取当前余额
alice_balance = int(r.get('account:alice') or 0)
bob_balance = int(r.get('account:bob') or 0)
# 检查是否足够转账
if alice_balance < 50:
r.unwatch()
print("余额不足,退出")
break
# 开启事务
pipe = r.pipeline()
pipe.decrby('account:alice', 50)
pipe.incrby('account:bob', 50)
result = pipe.execute()
# 如果 result 为 None,说明事务失败,重试
if result is None:
continue # 重新开始循环,重新 WATCH
else:
print("转账成功")
break
except Exception as e:
print(f"发生异常:{e}")
break
这个例子展示了如何在代码中正确处理 WATCH 事务失败的情况。
Watch 命令的适用场景
| 场景 | 是否适合使用 Watch |
|---|---|
| 多个客户端同时修改同一份配置文件 | ✅ 适合 |
| 账户余额增减(如转账) | ✅ 强烈推荐 |
| 库存扣减(如电商下单) | ✅ 推荐 |
| 读多写少的缓存更新 | ✅ 适合 |
| 高频短事务(如点赞) | ⚠️ 谨慎使用,冲突率高时性能下降 |
| 事务中包含复杂逻辑或耗时操作 | ❌ 不推荐,应避免长时间持有 WATCH |
总结与最佳实践
Redis Watch 命令 是实现分布式原子操作的关键工具,尤其适合需要“读-改-写”三步操作的场景。它通过乐观锁机制,在不阻塞其他客户端的前提下,保证了数据的一致性。
最佳实践建议:
- Always watch before multi:确保
WATCH在MULTI之前。 - Handle EXEC return value:检测
EXEC是否返回nil,失败则重试。 - Keep transaction small:事务越短,冲突概率越低。
- Don’t watch unnecessary keys:只监视真正相关的键,避免过度监视。
- Use retry mechanism in code:在应用层实现自动重试逻辑。
Redis Watch 命令虽然看似简单,但掌握它,你就拥有了在高并发环境下安全操作共享数据的能力。它不是“万能锁”,但却是构建可靠分布式系统的基石之一。当你下次需要“安全地修改多个键”时,别忘了这个默默守护你的“监视器”。