← 中文资源

简体中文版本

Redis 分布式锁获取与释放

用 Redis SET NX 加过期实现跨实例互斥锁:随机令牌校验后释放防误删,租期 watchdog 续期,业务函数被锁安全包裹。

阅读英文原文

示例说明

多实例同时触发定时任务是典型的重复执行事故源。SET key token NX PX 的原子性保证只有一个实例能拿到锁;过期时间是崩溃保险——持锁进程死亡后锁自动开放,不会死锁。释放时的比对删除是正确性关键:锁可能已过期并被他人持有,凭令牌校验只删自己的锁,避免误伤。

技术要点

  • SET NX PX 原子获取带过期锁
  • Lua 比对令牌防止误删他人锁
  • 续期定时器防止长任务锁过期

代码解读

acquireLock 生成随机令牌并原子写入;releaseLock 的 Lua 脚本把 GET 比对与 DEL 合并为原子操作。withLock 组合三者:拿锁后启动每三分之一个 TTL 续期一次的 watchdog,finally 里清理定时器并凭令牌释放。拿不到锁返回 null 由调用方决策跳过。

使用建议

锁只防重复执行不防并发读脏数据,必要时叠加版本号。TTL 与续期窗口要大于最坏任务时长。更强的保证可评估 Redlock 或 etcd 租约方案。

代码示例

import { createClient } from 'redis';

const redis = createClient({ url: 'redis://127.0.0.1:6379' });
await redis.connect();

function randomToken() {
  return crypto.randomUUID();
}

async function acquireLock(key: string, ttlMs: number) {
  const token = randomToken();
  const ok = await redis.set('lock:' + key, token, { NX: true, PX: ttlMs });
  return ok ? token : null;
}

async function releaseLock(key: string, token: string) {
  const script = `
    if redis.call("GET", KEYS[1]) == ARGV[1] then
      return redis.call("DEL", KEYS[1])
    else
      return 0
    end
  `;
  return (await redis.eval(script, {
    keys: ['lock:' + key],
    arguments: [token],
  })) === 1;
}

async function withLock<T>(
  key: string, ttlMs: number, fn: () => Promise<T>,
): Promise<T | null> {
  const token = await acquireLock(key, ttlMs);
  if (token === null) return null;

  const renew = setInterval(() => {
    redis.expire('lock:' + key, ttlMs / 1000).catch(() => {});
  }, ttlMs / 3);

  try {
    return await fn();
  } finally {
    clearInterval(renew);
    await releaseLock(key, token);
  }
}

const result = await withLock('billing:nightly', 30_000, async () => {
  console.log('nightly billing started under lock');
  return { invoices: 42 };
});
console.log('lock result:', result?.invoices ?? 'busy');