TypeScript实现资源锁系统遇问题:请求未按顺序执行(附测试用例)
资源串行执行锁函数问题排查与修复
问题背景
服务器接收并发请求时,部分读写资源无法同时访问,且数据库层实现锁不现实,因此需要实现一个将并行请求转为串行执行的函数。思路是为每个资源ID维护一个队列:请求到达时,若对应资源ID的队列为空则执行目标函数,否则将函数加入队列等待前面任务完成后再运行。但实际运行时,任务执行顺序随机,无法保证请求发起的顺序(期望结果数组为[1,2,3],实际顺序混乱)。
原代码如下:
export const sleep = (ms: number): Promise<void> => new Promise((resolve) => { setTimeout(resolve, ms); }); type AsyncFn = () => Promise<void>; const locks = new Map<string, AsyncFn[]>(); const unstackLock = async (id: string) => { const stack = locks.get(id); if (!stack) { return; } const nextFn = stack.shift(); if (!nextFn) { return; } try { await nextFn(); } finally { await unstackLock(id); } }; export const withLock = async (id: string, fn: AsyncFn): Promise<void> => { if (!locks.has(id)) { locks.set(id, []); } const lock = locks.get(id); if (!lock) { // never happens but makes TS happy throw new Error('Lock is not defined'); } lock.push(fn); if (lock.length === 1) { await unstackLock(id); } }; const test = async () => { const results: number[] = []; const f1 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(1); }); const f2 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(2); }); const f3 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(3); }); await Promise.all([f1, f2, f3]); if (results[0] !== 1 || results[1] !== 2 || results[2] !== 3) { console.log('FAILED', results); } else { console.log('SUCCESS'); } }; test();
问题分析
核心问题是**withLock函数的返回时机错误**:
- 当队列长度大于1时,
withLock不会等待当前任务执行完成,直接返回已resolve的Promise,导致Promise.all错误认为后续任务已完成。 - 队列中的任务虽然会串行执行,但
f2、f3对应的Promise已经提前resolve,Promise.all无需等待它们的任务实际执行完毕就会进入结果判断,而任务内部的sleep随机时长会打乱results的插入顺序。
修复方案
将队列改为维护Promise链,每个新任务都链接到前一个任务的Promise之后,确保串行执行的同时,withLock返回的Promise会等待当前任务完成后再resolve。
修复后的代码:
export const sleep = (ms: number): Promise<void> => new Promise((resolve) => { setTimeout(resolve, ms); }); type AsyncFn = () => Promise<void>; // 维护每个资源的Promise链,而非函数队列 const locks = new Map<string, Promise<void>>(); export const withLock = async (id: string, fn: AsyncFn): Promise<void> => { // 获取当前资源的Promise链,无则初始化为已resolve状态 const currentChain = locks.get(id) || Promise.resolve(); // 创建新的Promise:等待当前链完成后执行目标函数 const newChain = currentChain .then(async () => await fn()) .catch(err => console.error(`任务执行失败: ${err}`)); // 捕获错误避免中断后续任务 // 更新资源对应的Promise链 locks.set(id, newChain); // 等待当前任务执行完成 await newChain; // 清理已完成的链,避免内存泄漏 if (locks.get(id) === newChain) { locks.delete(id); } }; const test = async () => { const results: number[] = []; const f1 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(1); }); const f2 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(2); }); const f3 = withLock('lock', async () => { await sleep(Math.random() * 100); results.push(3); }); await Promise.all([f1, f2, f3]); if (results[0] !== 1 || results[1] !== 2 || results[2] !== 3) { console.log('FAILED', results); } else { console.log('SUCCESS'); } }; test();
修复逻辑说明
- Promise链维护:每个资源ID对应一条Promise链,新任务始终追加到链的末尾,严格保证串行执行顺序。
- 等待任务完成:
withLock返回的Promise会等待当前任务执行完毕才resolve,Promise.all会正确等待所有任务完成后再进行结果判断。 - 错误处理:捕获任务执行错误,避免单个任务失败中断整个串行链(可根据业务需求调整错误处理逻辑)。
- 内存清理:当当前任务是链的最后一个时,删除资源对应的锁,避免长期未使用的资源占用内存。
内容的提问来源于stack exchange,提问作者djfm
相关产品推荐
相关产品推荐

