锁是一份带失败窗口的租约
持有者用一个较短的 TTL 和唯一 owner token 获取键。其他竞争者在键过期或被释放前无法获取。持有者崩溃时,TTL 让系统继续推进。但如果持有者暂停——GC、虚拟机冻结、网络延迟——它可能在 TTL 之后恢复,仍以为自己持有锁,而另一个进程此时也已持有。
const token = crypto.randomUUID();
const acquired = await redis.set('lock:account:42', token, 'PX', 30_000, 'NX');
if (!acquired) return { acquired: false };
// Release only if the token still matches.
await redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
1,
'lock:account:42',
token
);
比较并删除的释放逻辑能避免删掉其他持有者的锁,但不能阻止过期持有者继续写入受保护资源。这需要围栏机制。
围栏才是安全机制
每次获取锁都分配一个单调递增的令牌,并把它包含在每次写入中。资源拒绝低于已见最高值的令牌。过期持有者可能仍以为自己有锁,但它的写入会失败。数据库序列、共识存储或对象版本都能提供令牌;随机 UUID 无法排序。
续租与活性
长任务需要看门狗在过期前续租,并且在任务取消或进程不健康时停止续租。没有续租的锁会在合法长任务期间过期,让第二个持有者进入;TTL 过长的锁则会在崩溃后阻塞进度。根据任务时长和续租节奏选择 TTL,并测试暂停场景。
何时使用
当重复执行有害,而受保护资源自身无法强制互斥时,使用分布式锁做协调。优点是跨进程互斥;代价是租约时序、时钟假设和围栏需求。与数据库唯一约束或乐观版本检查相比,锁更复杂、更容易出错;资源级防护可用时应优先采用。