问题

单机环境中,多个线程用语言层面的锁(如 mutex)就能互斥访问共享资源。

但在分布式环境中,多个进程/机器需要协调对共享资源的访问(如防止定时任务被多个实例同时执行、防止库存超卖)。

这就需要分布式锁。

基本要求

一、互斥性:任一时刻只有一个客户端能持有锁。

二、避免死锁:持有锁的客户端崩溃后,锁必须能被释放(通常需要过期时间)。

三、容错:锁服务本身要高可用。

基于 Redis 的实现

基本思路:用 SET 命令的 NX(只在不存在时设置)和 PX(设置过期时间)选项。

SET lock_key unique_value NX PX 30000
  • 返回 OK:获取锁成功;
  • 返回 nil:锁被别人持有。

释放锁:不能简单地 DEL,因为可能误删别人的锁(如果自己的锁已过期,而别人已经获取了)。

正确做法是用 Lua 脚本保证原子性:

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

即"检查值是否是我设置的,是才删除"。

过期时间的问题

设定多久合适?

  • 太短:任务还没执行完,锁就过期了,另一个客户端获得锁 → 互斥失效;
  • 太长:客户端崩溃后,锁要很久才能释放。

改进:看门狗(Watchdog)

获取锁后,启动一个后台线程,在锁快过期时自动续期(如果任务还在执行)。

客户端正常结束时主动释放。

这是 Redisson 等客户端库的做法。

Redlock 算法

Redis 作者提出的一种多节点方案:

  1. 向 N 个独立的 Redis 节点(通常 N=5)依次请求加锁(用相同的 key 和随机值);
  2. 只有在超过半数(N/2 + 1)的节点上成功,且总耗时小于锁的有效时间,才算获取成功;
  3. 释放时,向所有节点发送释放请求。

目的:避免单点 Redis 故障导致锁失效。

争议:分布式系统专家 Martin Kleppmann 曾撰文质疑 Redlock 在时钟漂移和GC 停顿场景下的安全性。双方有过深入讨论。

实践结论:Redlock 增加了复杂度,但在多数业务场景中,单 Redis 实例(配合合理的主从切换策略)已经够用。要明确的是——分布式锁不能保证 100% 的互斥,对强一致要求的场景,应该考虑基于共识算法(如 etcd、ZooKeeper)的方案,或者从设计上避免对锁的依赖。

基于数据库的实现

另一种方案是用数据库的唯一索引或行锁。

优点:不需要引入额外组件。

缺点:

  • 性能较差(每次加锁都是一次数据库操作);
  • 数据库的锁与事务耦合,容易出问题;
  • 数据库本身可能成为瓶颈。

一个设计建议

很多时候,可以避免用分布式锁:

  • 用乐观锁(版本号/CAS):更新时带上版本号,失败则重试。适合冲突不频繁的场景;
  • 用消息队列串行化:把并发操作放入队列,由单个消费者处理;
  • 用数据库的原子操作(如 UPDATE ... WHERE stock > 0)。

分布式锁应该是最后的选择——它引入了对外部系统的强依赖,且难以做到完全正确。

说明:本文为分布式系统的知识整理。