You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();

修复逻辑说明

  1. Promise链维护:每个资源ID对应一条Promise链,新任务始终追加到链的末尾,严格保证串行执行顺序。
  2. 等待任务完成:withLock返回的Promise会等待当前任务执行完毕才resolve,Promise.all会正确等待所有任务完成后再进行结果判断。
  3. 错误处理:捕获任务执行错误,避免单个任务失败中断整个串行链(可根据业务需求调整错误处理逻辑)。
  4. 内存清理:当当前任务是链的最后一个时,删除资源对应的锁,避免长期未使用的资源占用内存。

内容的提问来源于stack exchange,提问作者djfm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 10:20:41