分布式锁的 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 步骤如下:
- 客户端记录当前时间戳
t1。 - 向 N 个独立 Redis 节点发送
SET lock_key unique_value EX expire_time NX请求。 - 如果在大多数节点(≥ N/2+1)上成功获取锁,则继续下一步;否则失败。
- 计算实际耗时
t2 - t1,如果超过锁的有效时间(例如 10 秒),则认为锁无效,立即释放所有节点上的锁。 - 如果成功获取锁,客户端即可执行临界区代码。
- 执行完成后,必须手动释放所有节点上的锁(避免遗漏)。
⚠️ 注意: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 延长锁时间 |
| 异步释放 | 使用 setTimeout 或 setInterval 监控锁生命周期 |
| 日志追踪 | 为每个锁添加 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)。
希望这篇讲座式的文章对你有所帮助。如果你还有疑问,欢迎留言交流!