分布式锁的 Node.js 实现:基于 Redis 的 Redlock 算法

分布式锁的 Node.js 实现:基于 Redis 的 Redlock 算法详解

大家好,我是你们的技术讲师。今天我们要深入探讨一个在分布式系统中非常关键的话题——分布式锁,特别是使用 Redis + Redlock 算法 来实现高可靠性的分布式锁机制。

如果你正在开发微服务架构、多节点部署的应用程序,或者遇到多个进程/服务同时操作共享资源的问题(比如库存扣减、订单创建等),那么你一定需要了解并掌握这个技术。


一、什么是分布式锁?

在单机环境下,我们可以用 Java 的 synchronized 或者 Node.js 的 fs.readFileSync 这类原子操作来保证线程安全。但在分布式环境中,多个服务实例运行在不同机器上,它们无法直接通过内存或文件锁来同步访问共享资源。

这时候就需要一种跨进程、跨机器的“锁”机制 —— 分布式锁

它的核心目标是:

  • 互斥性:同一时刻只有一个客户端能持有锁;
  • 可重入性(可选):同一个客户端可以多次获取同一把锁而不死锁;
  • 容错性:即使某个节点宕机,也不会导致死锁;
  • 高性能:加锁和释放锁的延迟尽可能低;
  • 公平性(可选):按请求顺序分配锁。

二、为什么选择 Redis 实现分布式锁?

Redis 是一个高性能的键值存储数据库,具备以下优势:

特性 说明
原子性 使用 SETNX(Set if Not eXists)命令保证设置锁的原子性
持久化 支持 RDB/AOF,防止数据丢失
高可用 可以通过哨兵或集群模式提供冗余备份
简洁易用 提供丰富的 Lua 脚本支持,便于实现复杂逻辑

但要注意:单纯用 Redis 的 SET key value EX 30 NX 并不能完全满足分布式锁的所有要求,尤其在 Redis 主从切换时可能出现“脑裂”问题(即两个客户端同时拿到锁)。这就是为什么我们需要更高级的算法 —— Redlock


三、Redlock 算法简介

Redlock 是由 Redis 的作者 Salvatore Sanfilippo 在 2014 年提出的一种改进型分布式锁算法,旨在解决单 Redis 实例故障带来的风险。

核心思想:

不依赖单一 Redis 实例,而是让客户端向多个独立的 Redis 节点(至少 5 个)尝试获取锁,只有当多数节点成功获得锁后,才算真正拿到锁。

这样即使部分节点宕机或网络分区,也能保证锁的安全性和可用性。

Redlock 步骤如下:

  1. 客户端记录当前时间戳 t1
  2. 向 N 个独立 Redis 节点发送 SET lock_key unique_value EX expire_time NX 请求。
  3. 如果在大多数节点(≥ N/2+1)上成功获取锁,则继续下一步;否则失败。
  4. 计算实际耗时 t2 - t1,如果超过锁的有效时间(例如 10 秒),则认为锁无效,立即释放所有节点上的锁。
  5. 如果成功获取锁,客户端即可执行临界区代码。
  6. 执行完成后,必须手动释放所有节点上的锁(避免遗漏)。

⚠️ 注意:Redlock 的设计初衷是为了应对 Redis 单点故障场景,但它也带来了复杂度提升,是否使用需根据业务容忍度权衡。


四、Node.js 中实现 Redlock 的完整代码示例

下面我们将用纯 JavaScript 编写一个轻量级的 Redlock 客户端模块,支持自动续期、超时检测和异常处理。

1. 安装依赖

npm install redis

2. 创建 Redlock 类(redlock.js)

const redis = require('redis');

class Redlock {
  constructor(redisClients) {
    this.clients = redisClients;
    this.quorum = Math.floor(this.clients.length / 2) + 1; // 多数派
    this.lockExpiry = 10 * 1000; // 锁过期时间(毫秒)
    this.retryDelay = 50; // 重试间隔
  }

  async acquire(lockKey, timeout = 30000) {
    const start = Date.now();
    const id = this.generateId(); // 唯一标识符(用于释放)
    const attempts = [];

    for (let i = 0; i < this.clients.length; i++) {
      const client = this.clients[i];
      try {
        const result = await client.set(
          lockKey,
          id,
          'EX',
          this.lockExpiry / 1000,
          'NX'
        );
        if (result === 'OK') {
          attempts.push({ success: true, client });
        } else {
          attempts.push({ success: false });
        }
      } catch (err) {
        console.error(`Failed to acquire lock on client ${i}:`, err.message);
        attempts.push({ success: false });
      }
    }

    const successes = attempts.filter(a => a.success).length;

    if (successes < this.quorum) {
      await this.release(lockKey, id); // 释放已获取的锁
      throw new Error('Failed to acquire lock: not enough nodes responded');
    }

    const elapsed = Date.now() - start;
    if (elapsed > this.lockExpiry) {
      await this.release(lockKey, id);
      throw new Error('Lock acquisition took too long');
    }

    return { lockKey, id, release: () => this.release(lockKey, id) };
  }

  async release(lockKey, id) {
    const script = `
      if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
      else
        return 0
      end
    `;

    const results = [];
    for (const client of this.clients) {
      try {
        const res = await client.eval(script, 1, lockKey, id);
        results.push(res);
      } catch (err) {
        console.error(`Failed to release lock on client:`, err.message);
        results.push(0); // 忽略失败
      }
    }

    return results.filter(r => r === 1).length;
  }

  generateId() {
    return Math.random().toString(36).substring(2, 15) + Date.now().toString(36);
  }
}

3. 使用示例(app.js)

const { createClient } = require('redis');

// 初始化多个 Redis 客户端(模拟多节点)
const clients = [
  createClient({ host: 'localhost', port: 6379 }),
  createClient({ host: 'localhost', port: 6380 }),
  createClient({ host: 'localhost', port: 6381 }),
  createClient({ host: 'localhost', port: 6382 }),
  createClient({ host: 'localhost', port: 6383 }),
];

// 连接每个客户端
clients.forEach(client => client.connect());

// 创建 Redlock 实例
const redlock = new Redlock(clients);

async function criticalSection() {
  try {
    const lock = await redlock.acquire('my-resource-lock', 30000);
    console.log('🔒 Acquired lock:', lock.id);

    // 模拟临界区操作(如数据库更新)
    await new Promise(resolve => setTimeout(resolve, 2000));

    console.log('✅ Critical section completed.');
    lock.release(); // 显式释放锁
  } catch (err) {
    console.error('❌ Failed to acquire lock:', err.message);
  }
}

criticalSection();

五、Redlock 的优缺点分析

优点 缺点
✅ 抗单点故障:即使部分 Redis 节点宕机,仍能正常工作 ❌ 实现复杂:需要维护多个连接、状态同步、脚本编写
✅ 更高的可靠性:多数派机制减少误判概率 ❌ 性能开销大:每次加锁都要与多个节点通信
✅ 支持自动续期(可扩展) ❌ 时间漂移问题:NTP 不一致可能导致锁失效提前
✅ 适用于生产环境中的关键资源保护 ❌ 不适合高频短时锁(如每秒几百次请求)

📌 小贴士:如果你对性能要求极高且不怕单点故障,可以用简单的 Redis SETNX + TTL 方案;如果对安全性要求严格,Redlock 是更好的选择。


六、常见陷阱与最佳实践

❗ 陷阱 1:锁未正确释放导致死锁

// ❌ 错误做法:忘记调用 release()
const lock = await redlock.acquire('key');
doSomething();
// 没有 release() → 锁永远不释放!

✅ 正确做法:使用 try...finally 或 async/await 结构确保释放:

const lock = await redlock.acquire('key');
try {
  doSomething();
} finally {
  lock.release();
}

❗ 陷阱 2:锁过期时间设置不合理

  • 设置太短:可能在任务执行完之前被释放(如长事务);
  • 设置太长:若客户端崩溃,锁会一直占用资源。

✅ 推荐策略:将锁有效期设为任务预期最大执行时间的 2~3 倍,并结合心跳机制(续期)。

❗ 陷阱 3:Redis 时间漂移影响锁有效性

如果各节点之间的时间差超过几秒,Redlock 的时间判断就会出错。

✅ 解决方案:

  • 使用 NTP 对齐所有 Redis 节点时间;
  • 或者改用带有时间戳的 Lua 脚本进行精确比较。

七、进阶优化建议

功能 实现方式
自动续期 在锁有效期内定期调用 PEXPIRE 延长锁时间
异步释放 使用 setTimeoutsetInterval 监控锁生命周期
日志追踪 为每个锁添加 traceId,方便排查问题
测试工具 使用 redis-server --port 6379 --port 6380 ... 快速搭建测试环境

💡 示例:自动续期功能(简化版)

async acquireWithAutoRenew(lockKey, durationMs) {
  const lock = await this.acquire(lockKey, durationMs);
  const renewInterval = durationMs * 0.7; // 提前 30% 续期

  const renewTimer = setInterval(async () => {
    try {
      await this.clients[0].expire(lockKey, Math.ceil(durationMs / 1000));
    } catch (err) {
      console.warn('Failed to renew lock:', err.message);
    }
  }, renewInterval);

  lock.release = () => {
    clearInterval(renewTimer);
    return this.release(lockKey, lock.id);
  };

  return lock;
}

八、总结

今天我们从理论到实践,一步步讲解了如何用 Node.js 实现基于 Redis 的 Redlock 分布式锁算法:

  • 基础原理清晰:理解为何要引入 Redlock,以及它比简单 SETNX 更安全;
  • 代码结构严谨:提供了完整的类封装,易于集成进项目;
  • 实战指导明确:指出常见坑点和最佳实践,帮助你在生产中少踩雷;
  • 扩展性强:后续可加入自动续期、监控告警等功能。

🧠 最后提醒一句:分布式锁不是银弹!它只是解决并发控制的一种手段。在设计系统时,请优先考虑无锁方案(如幂等接口、消息队列、乐观锁)或更合适的数据库特性(如 PostgreSQL 的 SELECT FOR UPDATE)。

希望这篇讲座式的文章对你有所帮助。如果你还有疑问,欢迎留言交流!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注