
在单线程的JavaScript世界中,异步编程让并发操作成为可能。随着Node.js的普及和前端复杂度的提升,并发操作无处不在:从用户界面的多个异步请求到服务器端的数据库操作,再到微服务间的通信。然而,这种并发性带来了一个棘手问题——资源竞争(Race Condition)。
以我们的线上商城为例,两个用户同时点击"购买"按钮,库存检查同时进行,结果都显示有库存,导致超卖。或者多个异步操作同时修改同一个状态对象,最终状态变得不可预测。这些正是我在实际项目中多次遇到的痛点。
本文将深入探讨JavaScript中的异步互斥锁技术,特别是通过async-mutex库实现的高效解决方案。
资源竞争发生在多个异步操作同时访问/修改共享资源时,主要表现有:

let balance = 100;
async function withdraw(amount) {
const currentBalance = await getBalance(); // 同时读到100
await setBalance(currentBalance - amount);
}
// 并行执行withdraw(30)和withdraw(50),结果可能是70而非20场景类型 | 典型案例 | 风险等级 |
|---|---|---|
读写竞争 | 缓存更新时读取旧数据 | ⭐⭐⭐⭐ |
写写竞争 | 多节点同时更新配置 | ⭐⭐⭐⭐⭐ |
初始化竞争 | 单例重复初始化 | ⭐⭐ |
状态机竞争 | 订单状态机异常流转 | ⭐⭐⭐⭐ |
核心概念:通过锁机制确保任意时刻只有一个任务访问资源
// 使用async-mutex实现
import { Mutex } from 'async-mutex';
const resourceMutex = new Mutex();
let sharedData = { value: 0 };
/**
* 安全更新共享数据的异步函数
* 使用互斥锁确保对共享资源的独占访问,防止并发冲突
* @return {Promise<void>} 异步操作Promise,无具体返回值
*/
async function safeUpdate() {
// 获取锁(等待直到可用)
const release = await resourceMutex.acquire();
try {
// 临界区开始 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// 读取当前共享数据值
const temp = sharedData.value;
// 执行包含异步操作的复杂业务逻辑
await complexOperation();
// 基于初始值更新共享数据
sharedData.value = temp + 1;
// 临界区结束 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
} finally {
// 释放锁(在任何情况下都会执行)
release();
}
}操作流程:
架构解析:
Mutex实例:管理锁状态和等待队列。acquire():返回Promise,锁可用时resolve。release():释放锁并唤醒下一个等待者。设计要点:
try/finally确保异常时仍释放锁。注意事项:
功能升级:允许指定数量的并发访问
/**
* Semaphore类用于控制并发访问数量,通过令牌机制管理资源分配。
*/
class Semaphore {
/**
* 构造函数,初始化信号量实例。
* @param {number} concurrency - 初始并发令牌数量,默认为1(即互斥锁)
*/
constructor(concurrency = 1) {
// 当前可用令牌计数器
this.tokens = concurrency;
// 等待队列:存储因无令牌而阻塞的任务resolve函数
this.waitQueue = [];
}
/**
* 获取令牌:若有可用令牌则立即返回;否则阻塞直到其他任务释放令牌。
* @returns {Promise|void} 无令牌时返回Promise(需异步等待),有令牌时同步返回
*/
async acquire() {
// 存在可用令牌时直接消耗令牌
if (this.tokens > 0) {
this.tokens--;
return;
}
// 无可用令牌时创建Promise,将resolve存入等待队列
return new Promise(resolve => this.waitQueue.push(resolve));
}
/**
* 释放令牌:增加可用令牌数量,并唤醒等待队列中的第一个任务(若存在)。
*/
release() {
// 令牌计数器增加
this.tokens++;
// 检查等待队列并唤醒最早等待的任务
if (this.waitQueue.length > 0) {
const resolve = this.waitQueue.shift(); // 取出队列头部resolve函数
resolve(); // 执行resolve使对应acquire的Promise完成
}
}
}核心能力:
设计原则:
getWaitQueueSize() 等方法供监控系统使用应用场景:数据库连接池(限制最大10连接)
/**
* 使用信号量控制并发数执行数据库查询
*
* 该函数通过信号量机制限制同时执行的数据库查询数量,防止数据库过载。
* 每次查询前会获取信号量许可,查询完成后释放许可。
*
* @param {string} sql - 要执行的SQL查询语句
* @returns {Promise<any>} 返回数据库查询结果的Promise
* 解析值为数据库查询结果,拒绝时为查询错误
*/
const dbSemaphore = new Semaphore(10);
async function queryDatabase(sql) {
// 获取信号量许可(最多允许10个并发)
await dbSemaphore.acquire();
try {
// 执行数据库查询
return await db.query(sql);
} finally {
// 无论成功失败都释放信号量许可
dbSemaphore.release();
}
}核心优势:支持超时控制、可重入锁、多资源锁定
const AsyncLock = require('async-lock');
const lock = new AsyncLock({
timeout: 5000, // 5秒未获锁则超时
reentrant: true, // 允许同一上下文重入
});
/**
* 更新商品库存(带并发锁保护)
* 使用分布式锁确保同一商品库存更新的原子性操作
*
* @param {string|number} productId - 要更新的商品唯一标识符
* @param {number} quantity - 需要减少的库存数量(必须为正数)
* @returns {Promise} 返回库存更新操作的Promise结果
* @throws {Error} 当库存不足时抛出"库存不足"错误
*/
async function updateStock(productId, quantity) {
// 获取商品ID对应的锁后执行库存更新操作
return lock.acquire(productId, async () => {
// 获取当前库存量
const stock = await db.getStock(productId);
// 库存检查:当前库存需大于等于请求数量
if (stock < quantity) throw new Error('库存不足');
// 执行库存更新:减去指定数量
return db.updateStock(productId, stock - quantity);
});
}关键功能解析:
特性 | 说明 | 配置参数 |
|---|---|---|
键锁定 | 对不同资源独立加锁(如商品ID) | lockKey |
超时控制 | 避免死锁导致永久阻塞 | timeout |
可重入 | 同一异步上下文多次获取同一锁 | reentrant |
批量锁定 | 原子性锁定多个资源 | 键名数组 |
适用场景:简单数值类型的原子操作
// 创建共享缓冲区
const buffer = new SharedArrayBuffer(16);
const intArray = new Int32Array(buffer);
// 原子增加操作
Atomics.add(intArray, 0, 1);
// 原子比较交换
Atomics.compareExchange(intArray, 0, 10, 20);参数解析:
add(typedArray, index, value):原子加法compareExchange():CAS(比较并交换)操作Int32Array等类型化数组局限性:
方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
原生Mutex | 简单临界区保护 | 轻量级、零依赖 | 缺乏超时等高级功能 |
Semaphore | 资源池管理 | 支持并发度控制 | 实现复杂度较高 |
AsyncLock | 生产环境复杂场景 | 完备的错误处理机制 | 需引入外部依赖 |
原子操作 | 简单数值类型的原子操作 | 复杂度低 | 异步支持有限 |

// 错误示范:整个网络请求在锁内
await lock.acquire('key', async () => {
const data = await fetchData(); // 网络请求阻塞锁
});
// 正确做法:仅数据访问加锁
const data = await fetchData();
await lock.acquire('key', () => processData(data));
/*
* 尝试获取锁并执行任务,设置超时时间为3000毫秒
* 如果获取锁超时(错误信息为'AsyncLock - Timeout'),则触发降级策略
*
* @description
* - 使用分布式锁控制并发访问,确保临界区代码互斥执行
* - 超时机制防止长时间阻塞,超时后执行降级逻辑保证系统可用性
*
* @param {string} 'key' - 锁的唯一标识符
* @param {Function} task - 获取锁后需要执行的任务函数
* @param {Object} {timeout: 3000} - 配置选项,超时时间(毫秒)
*/
try {
await lock.acquire('key', task, { timeout: 3000 });
} catch (err) {
// 捕获锁操作过程中的异常
if (err.message === 'AsyncLock - Timeout') {
// 触发降级策略:当获取锁超时时的备用处理逻辑
}
}典型死锁场景:

解决方案:
// 定义全局锁获取顺序
const LOCK_ORDER = [lockA, lockB];
/**
* 执行需要多锁保护的安全操作
*
* 该函数按照预定义的全局锁顺序(LOCK_ORDER)依次获取所有锁,
* 执行操作后按相反顺序释放锁,避免死锁情况。
*
* 注意:该函数设计为无参数且无返回值
* @async
*/
async function safeOperation() {
// 顺序获取所有锁并存储释放函数
const releases = [];
for (const lock of LOCK_ORDER) {
releases.push(await lock.acquire());
}
// ...操作... (此处执行需要锁保护的业务逻辑)
// 按获取的逆序释放所有锁
releases.reverse().forEach(release => release());
}const release = await lock.acquireWithTimeout(300);
if (!release) {
// 回退已获资源
await rollback();
throw new DeadlockError();
}现象:操作不断重试但无法前进
解决方案:随机退避算法
/**
* 使用指数退避和随机抖动策略尝试获取互斥锁。
* 该函数会持续重试直到成功获取锁,每次重试的等待时间随尝试次数指数增长,并添加随机抖动以避免冲突。
*
* 返回:一个释放锁的函数(Promise解析后的值),调用该函数可以释放锁资源。
*/
async function acquireWithBackoff() {
// 记录当前尝试次数
let attempt = 0;
// 持续尝试直到成功获取锁
while (true) {
// 尝试获取互斥锁
const release = await mutex.tryAcquire();
// 成功获取则返回释放锁的函数
if (release) return release;
/**
* 计算退避时间:
* 1. 基础时间 = 2^attempt * 10ms(指数增长)
* 2. 叠加随机抖动 = [0, 50)ms 的随机值
* 避免多个等待进程同时重试
*/
const delay = 2 ** attempt * 10 + Math.random() * 50;
// 等待计算出的延迟时间
await new Promise(r => setTimeout(r, delay));
// 增加尝试计数
attempt++;
}
}场景:高优先级任务被低优先级任务阻塞
解决方案:
priorityMutex扩展import { PriorityMutex } from 'priority-mutex';
const mutex = new PriorityMutex();
// 高优先级任务
async function criticalTask() {
const release = await mutex.acquire({ priority: 'HIGH' });
// ...
}互斥锁作为解决JavaScript并发问题的核心武器,其价值不仅在于避免数据竞争,更在于为异步世界带来了确定性操作保障。
通过本文的探索,我们深入理解了:
锁是保证正确性的手段而非目的,合理缩小临界区、避免过度依赖锁,才能在高性能与数据一致性间取得完美平衡。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。