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
相关产品推荐
相关产品推荐

