网站Logo 90的blog

Redis分布式锁

root
2
2025-10-12

多个服务如何避免同时做同一件事

在单机程序里,多个 goroutine 同时修改一份数据时,可以用 sync.Mutex 加锁。

var mu sync.Mutex

func updateStock() {
	mu.Lock()
	defer mu.Unlock()

	// 修改库存
}

但如果服务部署了多个实例,例如两台机器都运行着同一个 Go 服务,sync.Mutex 就不够用了。因为每个进程都有自己独立的内存和锁,A 服务加锁后,B 服务并不知道。

这时就需要一个所有服务都能访问到的“公共锁”。Redis 分布式锁就是一种常见方案。

什么是 Redis 锁

Redis 锁的核心思路很简单:

  1. 谁先在 Redis 中成功写入一个锁标识,谁就获得锁;

  2. 没获得锁的请求等待、重试,或者直接返回“操作频繁”;

  3. 持有锁的服务完成任务后删除这个锁;

  4. 锁需要设置过期时间,避免服务异常时锁永远不释放。

例如用户抢购商品时,多个请求可能同时扣减库存。为了避免超卖,可以针对某个商品加锁:

lock:product:1001

拿到锁的请求才能继续扣库存,其他请求暂时不能进入这段逻辑。

最基础的 Redis 锁

Redis 中常用下面这条命令加锁:

SET lock:product:1001 unique-value NX EX 30

含义是:

  • lock:product:1001:锁的 key;

  • unique-value:锁的唯一标识;

  • NX:只有 key 不存在时才写入;

  • EX 30:锁 30 秒后自动过期。

如果命令返回 OK,说明加锁成功;如果返回空值,说明锁已经被其他服务持有。

这里的 NXEX 必须放在同一条命令中。这样“判断锁是否存在、创建锁、设置过期时间”是原子操作,可以避免并发时出现两个服务同时获得锁,或者锁创建后还没来得及设置过期时间就发生宕机。

为什么锁的值必须唯一

如果这样写:

SET lock:product:1001 locked NX EX 30

加锁本身可以正常工作,但释放锁时可能出问题。

设想下面的情况:

  1. 服务 A 获得锁,锁值为 locked,过期时间 30 秒;

  2. 服务 A 执行太久,30 秒后锁自动过期;

  3. 服务 B 获得了同一个锁,锁值也是 locked

  4. 服务 A 终于执行完成,执行 DEL lock:product:1001

  5. 服务 A 删除了服务 B 的锁。

这样服务 B 虽然还在处理业务,却失去了锁,其他请求可能同时进入临界区。

因此,锁的 value 应该是每一次获取锁时生成的唯一值,例如 UUID:

lock:product:1001 = 550e8400-e29b-41d4-a716-446655440000

释放锁时,必须先确认 Redis 中的 value 仍然是自己的,再删除。

为什么要使用 Lua 脚本

如果把“比较 value”和“删除 key”拆成两步,也会有并发风险:

1. GET lock:product:1001
2. 判断 value 是否是自己的
3. DEL lock:product:1001

问题在于:执行完第 2 步后,锁可能刚好过期并被其他服务重新获取。此时再执行第 3 步,仍然可能删掉别人的锁。

因此,需要让“判断 + 删除”成为原子操作。Redis 的 Lua 脚本可以做到这一点:

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

它的意思是:只有当锁的 value 和当前服务持有的唯一标识相同时,才删除锁。

锁过期时间该设置多久

锁的过期时间不能随便设置。

如果设置太短,业务还没完成,锁就过期了,其他服务可能进入临界区;如果设置太长,服务异常后其他请求要等待很久。

一个简单做法是,根据正常业务耗时设置一个略长的过期时间。例如业务通常在 1 秒内完成,可以设置 10 秒或 30 秒。

但对于耗时不确定的任务,例如大文件处理、批量同步、复杂报表生成,仅靠固定过期时间并不可靠。此时可以考虑:

  • 拆分任务,让临界区尽可能短;

  • 使用续期机制,在任务仍在正常执行时延长锁;

  • 使用成熟的分布式锁实现,例如 Redisson 的 watchdog 机制;

  • 重新评估是否真的需要用锁,能否通过数据库唯一约束、事务或消息队列保证一致性。

所以Redis 锁不是万能的。它适合控制并发进入某段逻辑,但库存、订单等核心数据的一致性,最终仍应由数据库事务、条件更新或唯一约束兜底。

总结

我对 Redis 分布式锁的理解可以概括为:

Redis 锁解决的是“多台机器、多个进程如何协调执行”的问题,但正确实现比“写一个 key”复杂得多。

一个相对可靠的 Redis 锁至少应该做到:

  • SET key value NX EX 原子加锁;

  • 每次加锁使用唯一 token;

  • 用 Lua 脚本原子地校验并释放锁;

  • 设置合理的过期时间;

  • 对核心数据继续依赖数据库事务、唯一约束或条件更新兜底。

真正做项目时,能不用分布式锁就不要滥用;但当多个服务确实需要协调访问同一份资源时,理解这些细节能避免很多隐蔽的并发问题

动物装饰