1.0版本+设计思路=> # 异步互斥锁
*设计思路在上一版本代码中,此处不再赘述。 迭代原因:在vue+web中正常使用,但在uniapp+APP/MP环境中,部分web端特有功能无法使用并且报错,导致整个工具函数死区。故为适配uniapp环境,对一些功能进行了妥协性的降级处理。
代码已上传npm,使用pnpm add jtwenty 可下载完整工具包(快速开发迭代中)*
异步任务互斥锁工具:解决前端并发控制的优雅方案
在复杂的前端应用中,如何优雅地控制异步任务的并发执行?一个健壮的互斥锁工具可能是答案。
概述
在前端开发中,我们经常面临异步任务并发控制的问题:表单重复提交、接口竞态条件、资源争用等。虽然防抖和节流技术可以解决部分高频触发问题,但当面对长时间运行的异步任务(如文件上传、复杂计算、多步提交)时,它们就显得力不从心。
Async Lock Manager 是一个专为前端环境设计的异步任务互斥锁工具,它采用互斥锁思想,通过任务名称对异步操作进行智能锁定,防止重复执行,同时提供超时控制、任务取消、队列管理等高级功能。
设计哲学:为什么需要专门的异步锁工具?
传统的前端异步处理方案存在以下局限:
- 防抖节流的不足:只能控制触发频率,无法防止长时间异步任务执行期间的重复触发
- 缺乏原子性保证:多个异步操作可能同时进入临界区,导致状态不一致
- 错误处理薄弱:一个任务失败可能影响整个业务流程
- 缺少取消机制:无法中止正在执行的异步操作
- 无排队管理:并发请求要么被拒绝,要么同时发送,缺乏有序执行
本工具的设计目标正是为了解决这些问题,提供:
- ✅ 可靠的互斥机制:确保同一时刻只有一个同名任务执行
- ✅ 完善的错误处理:支持超时、取消、队列满等错误类型
- ✅ 灵活的队列管理:支持任务排队和智能调度
- ✅ 跨平台兼容:适配 Web、uni-app 及各种小程序环境
- ✅ 资源自动清理:防止内存泄漏,确保系统稳定性
核心特性
1. 智能锁管理
每个锁通过唯一名称标识,只有获取锁的任务才能执行,后续同名任务会被拒绝或加入队列等待。
2. 超时控制
可配置执行超时时间,防止任务无限期占用锁资源。
3. 任务取消
支持手动取消正在执行的任务,释放锁资源。
4. 队列机制
当锁被占用时,后续任务可选择加入等待队列,按顺序执行。
5. 指数退避重试
对可重试的失败任务采用指数退避算法,避免频繁重试造成服务器压力。
6. 原子操作
通过多层检查确保锁获取的原子性,避免竞态条件。
7. 错误分类
将错误细分为重复执行、超时、取消、队列满等类型,便于针对性处理。
8. 性能监控
内置统计功能,监控任务执行情况,辅助性能优化。
安装与使用
基本用法
import { asyncLock } from './async-lock';
// 提交表单时防止重复提交
const submitForm = async (formData) => {
try {
const result = await asyncLock({
name: 'form-submit',
asyncFn: async (signal) => {
// signal可用于监听取消事件(Web环境)
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData),
signal // 传入AbortSignal以支持取消
});
return await response.json();
},
timeout: 8000, // 8秒超时
repeatTip: '正在提交,请勿重复操作',
onSuccess: (result) => {
console.log('提交成功:', result);
},
onFail: (error) => {
if (error.type === 'repeat') {
// 重复提交错误,通常已通过tipHandler提示用户
console.warn('请勿重复提交');
} else if (error.type === 'timeout') {
console.error('提交超时,请重试');
}
}
});
return result;
} catch (error) {
console.error('表单提交失败:', error);
throw error;
}
};
队列功能示例
// 启用队列功能,任务会按顺序执行
const processTasks = async (tasks) => {
const results = [];
for (const task of tasks) {
try {
const result = await asyncLock({
name: 'sequential-processing',
asyncFn: async () => {
return await processSingleTask(task);
},
enableQueue: true, // 启用队列
maxQueueSize: 10, // 最大队列长度
timeout: 5000
});
results.push(result);
} catch (error) {
if (error.code === 'QUEUE_FULL') {
console.error('系统繁忙,请稍后重试');
break;
}
}
}
return results;
};
高级配置
import { createLockManager } from './async-lock';
// 创建自定义锁管理器实例
const customLockManager = createLockManager({
timeout: 10000, // 默认超时10秒
repeatTip: '操作进行中,请稍候...',
throwRepeatError: false, // 不抛出重复错误,静默处理
autoCleanup: true, // 自动清理完成的锁
maxLockAge: 300000, // 锁最大存在时间5分钟
maxQueueSize: 20, // 队列最大长度
tipHandler: (message) => {
// 自定义提示处理器,可接入UI框架
showToast(message);
}
});
// 使用自定义管理器
customLockManager.execute({
name: 'critical-operation',
asyncFn: criticalAsyncFunction,
retryCount: 3, // 失败重试3次
baseRetryDelay: 1000, // 基础重试延迟1秒
retryCondition: (error) => {
// 自定义重试条件:只有网络错误才重试
return error.message.includes('Network') || error.code === 'NETWORK_ERROR';
}
});
跨平台适配策略
Web 环境
在 Web 环境中,工具利用标准的 AbortController API 实现真正的请求中断:
// Web环境下使用AbortController
asyncFn: async (signal) => {
const response = await fetch('/api/data', { signal });
if (signal.aborted) {
throw new Error('请求已被取消');
}
return response.json();
}
uni-app 及小程序环境
在非 Web 环境中,工具提供降级方案:
// uni-app环境下使用适配器
asyncFn: async (signal) => {
return new Promise((resolve, reject) => {
const requestTask = uni.request({
url: '/api/data',
success: resolve,
fail: reject
});
// 监听取消信号(非Web环境为模拟信号)
if (signal) {
const checkAbort = () => {
if (signal.aborted) {
requestTask.abort(); // 调用uni-app的abort方法
reject(signal.reason || new Error('已取消'));
}
};
// 对于模拟信号,需要轮询检查
const intervalId = setInterval(checkAbort, 100);
// 清理函数
signal._cleanup = () => clearInterval(intervalId);
}
});
}
推荐的请求封装模式
// utils/request-with-lock.js
export function createLockableRequest(lockManager) {
return async function requestWithLock(options) {
const { lockName, ...requestOptions } = options;
return lockManager.execute({
name: lockName,
asyncFn: async (signal) => {
// 统一处理不同环境的信号
return await platformSpecificRequest(requestOptions, signal);
},
timeout: requestOptions.timeout || 10000,
enableQueue: true
});
};
}
// 平台特定的请求实现
async function platformSpecificRequest(options, signal) {
// 判断环境并选择对应实现
if (typeof uni !== 'undefined') {
// uni-app环境
return uniAppRequest(options, signal);
} else {
// Web环境
return webRequest(options, signal);
}
}
实现原理
锁状态管理
工具使用 Map 数据结构存储所有锁的状态,确保 O(1) 时间复杂度的锁查找。每个锁包含以下信息:
- 锁定状态(locked)
- 创建时间(createdAt)
- 任务ID(taskId)
- 中断控制器(abortController,Web环境)
- 超时定时器(timeoutTimer)
原子性保证
通过三层检查确保锁获取的原子性:
- 初步检查锁状态
- 创建锁对象并二次检查
- 设置锁后的最终验证
队列管理
使用先进先出(FIFO)队列管理等待任务,支持:
- 队列长度限制
- 队列超时处理
- 智能任务调度
错误分类体系
const ERROR_TYPES = {
REPEAT: 'repeat', // 重复执行
TIMEOUT: 'timeout', // 执行超时
CANCEL: 'cancel', // 任务取消
QUEUE_FULL: 'queue_full', // 队列已满
QUEUE_TIMEOUT: 'queue_timeout', // 队列等待超时
LOCK_FAILED: 'lock_failed' // 获取锁失败
};
应用场景
场景1:表单提交防重复
防止用户在请求未完成时重复提交表单,特别是支付、订单提交等关键操作。
场景2:资源争用控制
管理对共享资源(如编辑权限、配置数据)的访问,确保同一时刻只有一个操作生效。
场景3:批量任务有序执行
控制批量任务(如文件批量上传、数据批量处理)的执行顺序,避免服务器过载。
场景4:实时数据同步
在实时数据更新场景中,防止并发更新导致的数据不一致问题。
完整源码
/**
* 异步任务互斥锁工具
* 核心能力:防止异步任务未完成时重复执行、超时控制、任务取消、资源自动清理
* 支持:队列机制、指数退避重试、原子操作、错误分类、性能监控
*/
class LockManager {
constructor(options = {}) {
this._lockMap = new Map();
this._queueMap = new Map();
this._defaults = {
timeout: 10000,
repeatTip: '操作中,请稍后...',
throwRepeatError: true,
autoCleanup: true,
maxLockAge: 5 * 60 * 1000,
maxQueueSize: 100,
enableStats: true,
tipHandler: () => {},
...options
};
this._stats = {
totalExecutions: 0,
successCount: 0,
timeoutCount: 0,
cancelCount: 0,
repeatRejectCount: 0,
queueFullCount: 0,
retryCount: 0
};
this._cleanupInterval = setInterval(
() => this._cleanupExpiredLocks(),
60000
);
this._processNextInQueue = this._processNextInQueue.bind(this);
}
/**
* 原子性地获取锁
*/
_acquireLock(name) {
const attemptId = `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const now = Date.now();
// 第一重检查
const existing = this._lockMap.get(name);
if (existing?.locked) {
return null;
}
// 创建新的锁对象
const lockItem = {
locked: true,
abortController: new AbortController(),
timeoutTimer: null,
createdAt: now,
taskId: `${name}_${now}_${Math.random().toString(36).slice(2, 10)}`,
attemptId: attemptId,
waitingQueue: this._queueMap.get(name) || []
};
// 第二重检查(原子性保障)
const current = this._lockMap.get(name);
if (current?.locked) {
return null;
}
// 设置锁(原子操作)
this._lockMap.set(name, lockItem);
// 最终验证
const afterSet = this._lockMap.get(name);
if (afterSet?.attemptId !== attemptId) {
lockItem.abortController.abort();
return null;
}
return lockItem;
}
/**
* 执行带锁的异步任务
*/
async execute(options) {
const {
name,
asyncFn,
onSuccess,
onFail,
repeatTip = this._defaults.repeatTip,
timeout = this._defaults.timeout,
throwRepeatError = this._defaults.throwRepeatError,
tipHandler = this._defaults.tipHandler,
enableQueue = false,
maxQueueSize = this._defaults.maxQueueSize,
retryCount = 0,
baseRetryDelay = 1000,
maxRetryDelay = 30000,
retryCondition = null,
autoCleanup = this._defaults.autoCleanup
} = options;
this._stats.totalExecutions++;
try {
const existingLock = this._lockMap.get(name);
if (existingLock?.locked) {
this._stats.repeatRejectCount++;
const repeatError = new Error(repeatTip);
repeatError.type = 'repeat';
repeatError.code = 'LOCKED';
repeatError.lockName = name;
tipHandler(repeatTip);
if (enableQueue) {
console.log(`任务【${name}】加入等待队列,当前队列长度:${this._queueMap.get(name)?.length || 0}`);
const queueOptions = {
...options,
enableQueue: false,
maxQueueSize: undefined
};
const queueResult = await this._addToQueue({
...queueOptions,
name,
maxQueueSize
});
onSuccess?.(queueResult);
return queueResult;
} else {
onFail?.(repeatError);
if (throwRepeatError) throw repeatError;
return Promise.reject(repeatError);
}
}
const result = await this._executeTask({
name,
asyncFn,
timeout,
retryCount,
baseRetryDelay,
maxRetryDelay,
retryCondition,
autoCleanup
});
this._stats.successCount++;
onSuccess?.(result);
return result;
} catch (error) {
switch (error.type) {
case 'timeout':
this._stats.timeoutCount++;
break;
case 'cancel':
this._stats.cancelCount++;
break;
case 'queue_full':
this._stats.queueFullCount++;
break;
}
onFail?.(error);
throw error;
}
}
/**
* 将任务加入等待队列
*/
_addToQueue(options) {
const { name, maxQueueSize = this._defaults.maxQueueSize } = options;
let queue = this._queueMap.get(name);
if (!queue) {
queue = [];
this._queueMap.set(name, queue);
}
if (queue.length >= maxQueueSize) {
this._stats.queueFullCount++;
const error = new Error(`任务队列【${name}】已满(最大${maxQueueSize})`);
error.type = 'queue_full';
error.code = 'QUEUE_FULL';
return Promise.reject(error);
}
return new Promise((resolve, reject) => {
const queueItem = {
options,
resolve,
reject,
enqueuedAt: Date.now()
};
queue.push(queueItem);
if (options.timeout > 0) {
queueItem.timeoutTimer = setTimeout(() => {
const index = queue.indexOf(queueItem);
if (index > -1) {
queue.splice(index, 1);
const error = new Error(`任务【${name}】在队列中等待超时`);
error.type = 'queue_timeout';
error.code = 'QUEUE_TIMEOUT';
reject(error);
if (queue.length === 0) {
this._queueMap.delete(name);
}
}
}, options.timeout);
}
});
}
/**
* 处理队列中的下一个任务
*/
async _processNextInQueue(name) {
const queue = this._queueMap.get(name);
if (!queue || queue.length === 0) {
this._queueMap.delete(name);
return;
}
await Promise.resolve();
const queueItem = queue.shift();
if (queueItem.timeoutTimer) {
clearTimeout(queueItem.timeoutTimer);
}
try {
const result = await this._executeTask(queueItem.options);
queueItem.resolve(result);
} catch (error) {
queueItem.reject(error);
} finally {
if (queue.length > 0) {
Promise.resolve().then(() => this._processNextInQueue(name));
} else {
this._queueMap.delete(name);
}
}
}
/**
* 执行任务核心逻辑
*/
async _executeTask(options) {
const {
name,
asyncFn,
timeout = this._defaults.timeout,
retryCount = 0,
baseRetryDelay = 1000,
maxRetryDelay = 30000,
retryCondition = null
} = options;
const lockItem = this._acquireLock(name);
if (!lockItem) {
const error = new Error(`无法获取锁【${name}】`);
error.type = 'lock_failed';
error.code = 'LOCK_FAILED';
throw error;
}
let result;
try {
if (timeout > 0) {
lockItem.timeoutTimer = setTimeout(() => {
const timeoutError = new Error(`任务【${name}】超时(${timeout}ms)`);
timeoutError.type = 'timeout';
timeoutError.code = 'TIMEOUT';
lockItem.abortController.abort(timeoutError);
}, timeout);
}
result = await this._executeWithExponentialBackoff(
() => asyncFn(lockItem.abortController.signal),
retryCount,
baseRetryDelay,
maxRetryDelay,
lockItem.abortController,
retryCondition
);
return result;
} catch (error) {
error.lockName = name;
error.taskId = lockItem.taskId;
throw error;
} finally {
this._cleanupLock(name, lockItem, options.autoCleanup ?? this._defaults.autoCleanup);
Promise.resolve().then(() => this._processNextInQueue(name));
}
}
/**
* 指数退避重试执行
*/
async _executeWithExponentialBackoff(fn, maxRetries, baseDelay, maxDelay, abortController, retryCondition) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (abortController.signal.aborted) {
const cancelError = new Error('任务已被取消');
cancelError.type = 'cancel';
cancelError.code = 'CANCELLED';
throw cancelError;
}
if (attempt > 0) {
const delay = this._calculateExponentialBackoffDelay(
attempt,
baseDelay,
maxDelay
);
this._stats.retryCount++;
console.log(`任务重试第${attempt}次,延迟${delay}ms`);
await this._sleep(delay, abortController.signal);
}
return await fn();
} catch (error) {
lastError = error;
if (!this._shouldRetry(error, retryCondition)) {
throw error;
}
if (attempt === maxRetries) {
error.retryAttempts = attempt;
throw error;
}
}
}
throw lastError;
}
/**
* 判断是否应该重试
*/
_shouldRetry(error, retryCondition) {
const noRetryTypes = ['cancel', 'timeout', 'queue_full', 'queue_timeout', 'lock_failed'];
if (noRetryTypes.includes(error.type)) {
return false;
}
if (typeof retryCondition === 'function') {
return retryCondition(error);
}
return true;
}
/**
* 计算指数退避延迟
*/
_calculateExponentialBackoffDelay(attempt, baseDelay, maxDelay) {
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
const jitter = exponentialDelay * 0.1 * Math.random();
return Math.min(exponentialDelay + jitter, maxDelay);
}
/**
* 可中断的延时
*/
_sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(new Error('等待被中断'));
return;
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', abortHandler);
resolve();
}, ms);
const abortHandler = () => {
clearTimeout(timer);
const error = new Error('等待被中断');
error.type = 'cancel';
error.code = 'SLEEP_CANCELLED';
reject(error);
};
signal.addEventListener('abort', abortHandler);
const cleanup = () => {
clearTimeout(timer);
signal.removeEventListener('abort', abortHandler);
};
this._safeFinally(() => {
cleanup();
}, resolve, reject);
});
}
/**
* 安全的finally执行
*/
_safeFinally(cleanupFn, resolve, reject) {
const wrappedResolve = (value) => {
try {
cleanupFn();
} finally {
resolve(value);
}
};
const wrappedReject = (error) => {
try {
cleanupFn();
} finally {
reject(error);
}
};
return { resolve: wrappedResolve, reject: wrappedReject };
}
/**
* 清理锁资源
*/
_cleanupLock(name, lockItem, autoCleanup) {
if (lockItem.timeoutTimer) {
clearTimeout(lockItem.timeoutTimer);
lockItem.timeoutTimer = null;
}
if (lockItem.abortController) {
lockItem.abortController = null;
}
if (autoCleanup) {
this._lockMap.delete(name);
} else {
lockItem.locked = false;
lockItem.abortController = null;
lockItem.timeoutTimer = null;
}
}
/**
* 清理过期锁和队列
*/
_cleanupExpiredLocks() {
const now = Date.now();
const maxAge = this._defaults.maxLockAge;
// 清理过期锁
for (const [name, lockItem] of this._lockMap.entries()) {
if (lockItem.locked && (now - lockItem.createdAt) > maxAge) {
console.warn(`清理过期锁【${name}】,已锁定${now - lockItem.createdAt}ms`);
const error = new Error('锁过期自动清理');
error.type = 'timeout';
error.code = 'LOCK_EXPIRED';
if (lockItem.abortController) {
lockItem.abortController.abort(error);
}
this._lockMap.delete(name);
}
}
// 清理过期队列项
for (const [name, queue] of this._queueMap.entries()) {
const expiredItems = [];
for (let i = 0; i < queue.length; i++) {
const item = queue[i];
const queueAge = now - item.enqueuedAt;
const timeout = item.options?.timeout || 30000;
if (queueAge > timeout) {
expiredItems.push(i);
}
}
for (let i = expiredItems.length - 1; i >= 0; i--) {
const index = expiredItems[i];
const item = queue[index];
if (item.timeoutTimer) {
clearTimeout(item.timeoutTimer);
}
const error = new Error(`任务【${name}】在队列中过期`);
error.type = 'queue_timeout';
error.code = 'QUEUE_TIMEOUT';
item.reject(error);
queue.splice(index, 1);
}
if (queue.length === 0) {
this._queueMap.delete(name);
}
}
}
/**
* 手动释放指定锁
*/
releaseLock(name) {
const lockItem = this._lockMap.get(name);
if (lockItem) {
this._cleanupLock(name, lockItem, true);
}
const queue = this._queueMap.get(name);
if (queue) {
queue.forEach(item => {
if (item.timeoutTimer) {
clearTimeout(item.timeoutTimer);
}
const error = new Error('锁被手动释放,队列任务取消');
error.type = 'cancel';
error.code = 'MANUAL_RELEASE';
item.reject(error);
});
this._queueMap.delete(name);
}
}
/**
* 批量释放所有锁
*/
releaseAllLocks() {
this._lockMap.forEach((lockItem, name) => {
this._cleanupLock(name, lockItem, true);
});
this._lockMap.clear();
this._queueMap.forEach((queue, name) => {
queue.forEach(item => {
if (item.timeoutTimer) {
clearTimeout(item.timeoutTimer);
}
const error = new Error('所有锁被释放,队列任务取消');
error.type = 'cancel';
error.code = 'ALL_RELEASED';
item.reject(error);
});
});
this._queueMap.clear();
}
/**
* 取消正在执行的任务
*/
cancelLockTask(name, reason = "用户主动取消") {
const lockItem = this._lockMap.get(name);
if (lockItem?.locked && lockItem.abortController) {
const error = new Error(reason);
error.type = 'cancel';
error.code = 'USER_CANCEL';
lockItem.abortController.abort(error);
this._cleanupLock(name, lockItem, true);
return true;
}
return false;
}
/**
* 获取指定任务的锁状态
*/
getLockStatus(name) {
const lockItem = this._lockMap.get(name);
const queue = this._queueMap.get(name);
return {
locked: lockItem?.locked ?? false,
taskId: lockItem?.taskId,
createdAt: lockItem?.createdAt,
age: lockItem ? Date.now() - lockItem.createdAt : 0,
hasAbortController: !!lockItem?.abortController,
queueLength: queue?.length || 0,
queueWaitTimes: queue?.map(item => Date.now() - item.enqueuedAt) || []
};
}
/**
* 获取统计信息
*/
getStats() {
return {
...this._stats,
activeLocks: Array.from(this._lockMap.entries())
.filter(([_, lock]) => lock.locked)
.map(([name, lock]) => ({
name,
age: Date.now() - lock.createdAt,
taskId: lock.taskId
})),
waitingQueues: Array.from(this._queueMap.entries())
.map(([name, queue]) => ({
name,
length: queue.length,
oldestWait: queue.length > 0 ? Date.now() - queue[0].enqueuedAt : 0
}))
};
}
/**
* 重置统计信息
*/
resetStats() {
this._stats = {
totalExecutions: 0,
successCount: 0,
timeoutCount: 0,
cancelCount: 0,
repeatRejectCount: 0,
queueFullCount: 0,
retryCount: 0
};
}
/**
* 销毁实例
*/
destroy() {
clearInterval(this._cleanupInterval);
this.releaseAllLocks();
this._queueMap.clear();
this._lockMap.clear();
}
}
// 创建锁管理器的工厂函数
export const createLockManager = (options) => new LockManager(options);
// 默认单例
export const defaultLockManager = new LockManager({
tipHandler: () => {}
});
// 带控制台警告的单例
export const verboseLockManager = new LockManager({
tipHandler: console.warn
});
// 核心方法导出(使用默认单例)
export const asyncLock = (options) => defaultLockManager.execute(options);
export const releaseLock = (name) => defaultLockManager.releaseLock(name);
export const releaseAllLocks = () => defaultLockManager.releaseAllLocks();
export const cancelLockTask = (name, reason) => defaultLockManager.cancelLockTask(name, reason);
export const getLockStatus = (name) => defaultLockManager.getLockStatus(name);
export const getStats = () => defaultLockManager.getStats();
export const resetStats = () => defaultLockManager.resetStats();
export const destroyLockManager = () => defaultLockManager.destroy();
// 导出类本身
export { LockManager };
总结
异步任务互斥锁工具是前端复杂应用中的重要基础设施,它解决了传统防抖节流无法处理的异步并发控制问题。通过合理的锁机制、队列管理和错误处理,可以显著提升应用的稳定性和用户体验。
在实际项目中,建议根据具体需求选择合适的配置,并确保在 uni-app 等跨平台环境中正确适配请求中断机制。对于简单的场景,可以使用默认单例;对于复杂场景,可以创建多个独立的锁管理器实例,分别管理不同业务域的异步任务。
扩展思考:随着前端应用复杂度的增加,类似的状态管理和并发控制工具将变得越来越重要。未来可以考虑将此工具与状态管理库(如 Pinia、Redux)深度集成,或开发可视化监控界面,进一步提升开发体验和系统可观测性。