导读:本期聚焦于黑豹创作的《Node.js如何实现分布式锁的超时自动续期?基于Redis Lua脚本保证原子性详解》,敬请观看详情。分布式锁用久了总会遇到一个尴尬场景:业务逻辑还没执行完,锁的过期时间先到了,另一个进程拿到锁并发写入,数据瞬间被搞乱。要让锁在持有时自动延长生命周期,核心思路是借助Redis的Lua脚本,把判断持有者与重置过期时间两个操作合并成一次原子执行,避免脚本执行中途被其他客户端插队。本文围绕Node.js环境,讲解为什么续期必须原子化、如何编写正确的Lua脚本、watchdog看门狗机制怎样设计,以及Redlock相关的常见误区,并给出可直接运行的代码示例与踩坑经验。

用Redis做分布式锁是Node.js服务里非常常见的方案,set key value nx ex这条命令几乎成了标配。但真正在生产环境跑起来之后,问题往往出在锁过期时间不好定:设短了业务没跑完锁就被释放,设长了进程崩溃后其他节点又要干等。超时自动续期(也叫看门狗机制)就是解决这个矛盾的方案,而它的核心难点在于续期动作必须保证原子性,这正是Lua脚本发挥作用的地方。

Node.js如何实现分布式锁的超时自动续期?基于Redis Lua脚本保证原子性详解

为什么锁续期必须用Lua脚本保证原子性

先看一段典型的错误续期代码,很多项目里都能找到类似的写法:

const token = uuid(); // 锁的唯一持有标识
await redis.set('mylock', token, 'EX', 30, 'NX');

// 业务执行到一半,准备续期
const owner = await redis.get('mylock');
if (owner === token) {
  // 危险窗口:get和expire之间锁可能刚好过期并被别人抢走
  await redis.expire('mylock', 30);
}

这段代码的问题在于getexpire是两条独立命令,中间存在时间窗口。假设在get执行之后、expire执行之前,锁恰好过期,另一个客户端B抢到了锁并设置了新的value,此时我们再执行expire,就会错误地把B刚刚持有的锁延长了过期时间。更糟糕的情况是,如果后续代码还有释放锁的逻辑(del操作前校验不严),可能出现A把B的锁删掉、C又拿到锁,最终两个进程同时持有锁,分布式锁彻底失效。

Redis的单条命令是原子的,但多条命令之间可能穿插其他客户端的请求。Lua脚本在Redis中是作为一个整体执行的,Redis保证脚本执行期间不会插入任何其他命令。也就是说,把“判断持有者”和“重置过期时间”写进同一个Lua脚本,客户端就无法在两个步骤之间插队,这就是续期必须借助Lua脚本的根本原因。需要注意原子性只针对单个Redis节点,如果部署的是Redis Cluster,key会被哈希到某个槽,脚本只要操作单个key就不受影响;跨多个key操作时要用hash tag保证它们落在同一节点。

续期Lua脚本的正确写法与Node.js集成

续期脚本的逻辑很简单:只有当前锁的value等于客户端传入的token时,才执行pexpire延长过期时间,否则返回0表示续期失败(锁已易主或已删除)。脚本如下:

-- KEYS[1] 锁的key,ARGV[1] 持有者token,ARGV[2] 新的过期毫秒数
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
  return 0
end

释放锁的脚本同理,也必须原子化,否则同样存在误删他人锁的风险:

if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
else
  return 0
end

在Node.js中可以通过ioredis的evaldefineCommand方法执行脚本。推荐用defineCommand把脚本注册成自定义命令,代码更干净,还能利用Redis脚本的缓存机制(EVALSHA)减少每次传输脚本的带宽开销:

const Redis = require('ioredis');
const redis = new Redis({ host: '127.0.0.1', port: 6379 });

const renewScript = `
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('PEXPIRE', KEYS[1], ARGV[2])
else
  return 0
end`;

const releaseScript = `
if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
else
  return 0
end`;

redis.defineCommand('renewLock', {
  numberOfKeys: 1,
  lua: renewScript
});
redis.defineCommand('releaseLock', {
  numberOfKeys: 1,
  lua: releaseScript
});

module.exports = { redis };

看门狗机制的设计与完整实现

有了原子续期能力,还需要一个后台定时任务来驱动它,这就是看门狗。它的基本工作方式是:加锁时不设固定长过期时间,或者设一个较短时间(比如30秒),然后每隔过期时间的三分之一(比如10秒)检查一次业务是否还在执行,是则续期。之所以选三分之一,是为了保证即使某次续期因为网络抖动失败了,后面还有两次重试机会,不至于锁直接过期。

看门狗的生命周期必须和业务绑定:业务开始时启动定时器,业务结束(无论成功还是失败)时立即清除定时器并释放锁。如果进程在业务执行中崩溃,定时器随进程一起消亡,锁会在过期后自动释放,不会出现死锁。下面是完整的封装实现:

const crypto = require('crypto');

class DistributedLock {
  constructor(redis, key, ttl = 30000) {
    this.redis = redis;
    this.key = key;
    this.ttl = ttl;
    this.token = null;
    this.timer = null;
  }

  // 尝试加锁
  async acquire() {
    this.token = crypto.randomUUID();
    const ok = await this.redis.set(this.key, this.token, 'PX', this.ttl, 'NX');
    if (ok !== 'OK') return false;
    this.startWatchdog();
    return true;
  }

  // 看门狗:每ttl/3续期一次
  startWatchdog() {
    this.timer = setInterval(async () => {
      try {
        // 调用原子续期,锁已易主则返回0
        const result = await this.redis.renewLock(this.key, this.token, this.ttl);
        if (result === 0) {
          this.stopWatchdog();
        }
      } catch (err) {
        console.error('续期失败,等待下次重试:', err.message);
      }
    }, this.ttl / 3);
    if (this.timer.unref) this.timer.unref();
  }

  stopWatchdog() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  // 业务执行完毕,释放锁
  async release() {
    this.stopWatchdog();
    if (this.token) {
      await this.redis.releaseLock(this.key, this.token);
      this.token = null;
    }
  }
}

// 使用示例:包装需要互斥执行的业务
async function withLock(redis, key, fn, ttl = 30000) {
  const lock = new DistributedLock(redis, key, ttl);
  if (!await lock.acquire()) {
    throw new Error('获取锁失败:' + key);
  }
  try {
    return await fn();
  } finally {
    await lock.release();
  }
}

(async () => {
  await withLock(redis, 'order:1001:pay', async () => {
    console.log('执行支付业务,即使超过30秒锁也不会失效');
    await new Promise(r => setTimeout(r, 60000));
  });
  redis.disconnect();
})();

这段实现里有几个细节值得注意。第一,token必须每个客户端唯一(UUID),它是续期和释放时证明身份的凭证,绝不能用固定字符串,否则会互相误删锁。第二,续期失败返回0时要立即停止看门狗,说明锁已经被别的进程持有,当前业务应该考虑中断或进入降级逻辑。第三,定时器建议调用unref(),避免它阻止Node.js进程正常退出。第四,Node.js的事件循环如果被长时间同步计算阻塞,定时器会延迟触发,极端情况下可能来不及续期,所以持锁的业务里不要有大计算量的同步代码,或者把续期间隔设置得再保守一些。

常见误区与生产环境注意事项

第一个误区是给锁设置一个超长的过期时间来“绕过”续期问题,比如直接设成10分钟。这种做法在进程崩溃时会导致锁最长悬挂10分钟,其他节点完全无法工作,等于牺牲了可用性换取表面的简单。看门狗加短TTL的组合才是兼顾方案:正常运行时锁不会提前失效,异常崩溃时锁最多等一个TTL就自动恢复。

第二个误区是忽略时钟和网络问题对续期的影响。续期请求在网络中传输需要时间,如果网络延迟接近或超过TTL的三分之一,续期窗口就会变得很紧张。建议TTL不要太小,一般10到30秒比较稳妥,同时给续期失败加监控告警,一旦发现续期失败率上升,通常是Redis或网络出了问题。

第三个误区涉及Redlock算法。如果业务要求强正确性,部署多个独立Redis节点用Redlock,续期逻辑就需要对多个节点分别执行并检查多数派成功,复杂度明显上升。对绝大多数互联网业务来说,单节点Redis加原子续期加看门狗已经足够,可以参考Redisson在Java生态中的实现思路:它同样是在锁被持有的情况下启动定时任务,每隔内部锁时间的 thirds之一执行Lua续期。Node.js生态里,redlock这个npm包也内置了续期支持,可以直接使用,其用法如下:

const Redlock = require('redlock');

const redlock = new Redlock([redis], {
  retryCount: 3,
  retryDelay: 200
});

async function run() {
  let lock = await redlock.acquire(['resource:1'], 30000);
  try {
    // redlock会在锁过期前自动续期
    const result = await doBusiness();
    return result;
  } finally {
    await lock.release();
  }
}

总结一下,Node.js实现分布式锁续期的关键点有三:用Lua脚本把身份校验和过期时间重置合成原子操作,用唯一token标识持有者防止误操作他人的锁,用与业务同生命周期的定时器驱动续期并在业务结束时清理。把这三点做扎实,分布式锁在超长任务场景下就能稳定工作了。

Node.js分布式锁Lua脚本锁续期修改时间:2026-09-13 17:23:05

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260913/56145.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。