You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJS中Redlock锁被多实例同时获取的问题咨询

多实例NestJS定时任务Redlock互斥失效原因及修复方案

问题原因分析

1. Redlock核心逻辑实现不规范

如果你的redisService里的锁方法没严格按Redlock要求实现,大概率会出问题:

  • 加锁时:如果分开执行SETNX和EXPIRE,会有竞态漏洞——比如SETNX成功后还没设过期就宕机,锁就永久卡住;或者没用唯一锁标识,导致释放锁时误删别人的锁。
  • 释放锁时:直接调用DEL删除锁,而不是先校验锁的唯一值再删除,就会出现A实例锁过期后,B实例刚拿到新锁,A实例执行DEL把B的锁删掉的情况。

2. 锁过期时间和任务周期太接近

你设的锁过期是1秒,刚好和Cron的1秒触发周期一致。多实例的系统时钟可能有微小漂移,或者NestJS的Cron调度有精度误差,刚好在锁过期的瞬间,两个实例同时发起加锁请求,就会同时拿到锁。

3. 单Redis节点的可靠性问题

Redlock算法本身需要多个独立Redis节点来避免单点故障,如果只依赖单个Redis,当Redis出现主从切换、网络分区时,会出现锁脑裂,多个实例同时拿到锁。

4. 任务执行/网络延迟导致锁提前过期

哪怕只是打日志,如果Redis网络响应慢,或者后续任务逻辑(如果有扩展)执行时间超过1秒,锁会自动过期,其他实例就会趁机抢锁执行。


修复方案

1. 规范锁的实现逻辑

必须用原子命令加锁,用Lua脚本原子释放锁:

  • 加锁:生成唯一锁标识(比如UUID),用SET lockKey uniqueValue NX PX expireMs原子命令,确保加锁和设过期一步完成:
    async acquireLock(key: string, expireMs: number): Promise<string | null> {
      const lockValue = uuid.v4(); // 生成唯一锁值
      const result = await this.redisClient.set(key, lockValue, {
        NX: true, // 只有key不存在时才设置
        PX: expireMs, // 自动过期时间
      });
      return result === 'OK' ? lockValue : null;
    }
    
  • 释放锁:用Lua脚本先校验锁值再删除,避免误删:
    async releaseLock(key: string, lockValue: string): Promise<void> {
      const luaScript = `
        if redis.call("GET", KEYS[1]) == ARGV[1] then
            return redis.call("DEL", KEYS[1])
        else
            return 0
        end
      `;
      await this.redisClient.eval(luaScript, {
        keys: [key],
        args: [lockValue],
      });
    }
    

2. 调整锁过期时间

把锁过期时间设为任务周期+最大执行时间的1.5-2倍,比如你的任务1秒执行一次,锁过期设为2-3秒,避开调度和网络延迟的临界点。

3. 增加锁获取重试机制

加锁失败时不要直接放弃,短时间内重试几次(比如间隔100ms重试2次),减少瞬时竞争导致的失效:

async test(): Promise<void> {
  let lockValue: string | null = null;
  const maxRetries = 2;
  let retryCount = 0;

  while (retryCount <= maxRetries && !lockValue) {
    try {
      lockValue = await this.redisService.acquireLock('test-cron-job-lock', 2000);
      if (lockValue) break;
      await new Promise(resolve => setTimeout(resolve, 100));
      retryCount++;
    } catch (e) {
      retryCount++;
    }
  }

  if (!lockValue) {
    this.logger.error('Failed to acquire lock after retries');
    return;
  }

  try {
    this.logger.log(`Cron job at ${this.getTimeWithoutMilliseconds()}`);
  } finally {
    await this.redisService.releaseLock('test-cron-job-lock', lockValue).catch(() => {});
  }
}

4. 用成熟的Redlock库替代自定义实现

直接用redlock npm包,它已经封装了标准的Redlock逻辑,避免自己实现的漏洞:

import Redlock from 'redlock';

// 初始化Redlock,建议配置多个Redis节点提升可靠性
private redlock = new Redlock(
  [this.redisClient],
  {
    driftFactor: 0.01, // 时钟漂移容忍度
    retryCount: 3,
    retryDelay: 200,
    retryJitter: 200,
  }
);

@Cron('*/1 * * * * *')
async test(): Promise<void> {
  let lock;
  try {
    lock = await this.redlock.lock('test-cron-job-lock', 2000);
    this.logger.log(`Cron job at ${this.getTimeWithoutMilliseconds()}`);
  } catch (e) {
    this.logger.error('Error in test cron job');
  } finally {
    if (lock) {
      await lock.unlock().catch(() => {}); // 释放锁,忽略异常
    }
  }
}

5. 同步多实例系统时钟

用NTP服务把所有实例的系统时钟同步,减少调度时间的偏差,降低锁过期临界点的竞争概率。


内容的提问来源于stack exchange,提问作者rook

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 19:25:54