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

如何在Node/Express+TypeScript中锁定代码临界区?

单实例Node/Express + TypeScript场景下的多键临界区锁定方案

针对你提到的单服务器单数据库场景,不需要分布式锁,只需要解决异步IO并发导致的请求冲突问题,这里有两个实用的方案,都是TypeScript友好的:

方案一:用async-mutex(推荐,成熟可靠)

你之前关注的那些包要么维护状态差要么缺少TypeScript支持,但async-mutex是个例外——它周下载量很高,维护活跃,自带完整的TypeScript类型定义,还提供了灵活的锁机制,完全能满足你同时锁定多个资源(比如FOO和BAR)的需求。

快速上手

先安装包:

npm install async-mutex

然后针对你的FOO更新场景写代码:

import { Mutex } from 'async-mutex';
import { Request, Response } from 'express';

// 为每个需要保护的资源创建独立的互斥锁
const resourceLocks = new Map<string, Mutex>([
  ['FOO', new Mutex()],
  ['BAR', new Mutex()]
]);

// 工具函数:按固定顺序获取多个锁(核心!避免死锁)
async function getLocks(keys: string[]): Promise<(() => void)[]> {
  // 对锁键排序,确保所有请求都按相同顺序获取锁,防止死锁
  const sortedKeys = [...keys].sort();
  const releaseFunctions: (() => void)[] = [];

  for (const key of sortedKeys) {
    const mutex = resourceLocks.get(key);
    if (!mutex) throw new Error(`No lock configured for resource: ${key}`);
    // 获取锁,得到释放函数
    const release = await mutex.acquire();
    releaseFunctions.push(release);
  }

  return releaseFunctions;
}

// 你的FOO更新接口处理函数
async function handleFooUpdate(req: Request, res: Response) {
  let releaseLocks: (() => void)[] = [];
  try {
    // 同时获取FOO和BAR的锁
    releaseLocks = await getLocks(['FOO', 'BAR']);

    // 临界区操作:读取旧数据、验证、写入新数据
    const oldFoo = await yourDbClient.query('SELECT * FROM foo WHERE id = ?', [req.params.id]);
    const relatedBar = await yourDbClient.query('SELECT * FROM bar WHERE foo_id = ?', [oldFoo.id]);

    // 替换成你的业务验证逻辑
    if (!isValidFooUpdate(req.body, oldFoo, relatedBar)) {
      return res.status(400).json({ error: 'Invalid update request' });
    }

    // 写入更新后的FOO
    await yourDbClient.query('UPDATE foo SET ? WHERE id = ?', [req.body, req.params.id]);

    res.status(200).json({ success: true, data: oldFoo });
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  } finally {
    // 不管成功失败,都要释放所有锁
    releaseLocks.forEach(release => release());
  }
}

方案二:自行实现轻量多键锁(无依赖,可控性强)

如果你不想引入第三方包,也可以自己写一个极简的多键内存锁,逻辑简单,完全适配单实例场景:

import { Request, Response } from 'express';

class MultiKeyMutex {
  private activeLocks = new Map<string, Promise<void>>();

  async acquire(key: string): Promise<() => void> {
    // 等待当前该键的锁释放
    while (this.activeLocks.has(key)) {
      await this.activeLocks.get(key);
    }

    let resolveRelease: () => void;
    // 创建新的锁Promise
    const lockPromise = new Promise<void>(resolve => {
      resolveRelease = resolve;
    });

    this.activeLocks.set(key, lockPromise);

    // 返回释放函数
    return () => {
      this.activeLocks.delete(key);
      resolveRelease!();
    };
  }

  // 批量获取锁,同样按排序避免死锁
  async acquireMultiple(keys: string[]): Promise<(() => void)[]> {
    const sortedKeys = [...keys].sort();
    const releases: (() => void)[] = [];

    for (const key of sortedKeys) {
      const release = await this.acquire(key);
      releases.push(release);
    }

    return releases;
  }
}

// 实例化锁管理器
const lockManager = new MultiKeyMutex();

// 同样的FOO更新处理函数
async function handleFooUpdate(req: Request, res: Response) {
  let releaseLocks: (() => void)[] = [];
  try {
    releaseLocks = await lockManager.acquireMultiple(['FOO', 'BAR']);

    // 临界区操作:读数据、验证、写数据
    const oldFoo = await yourDbClient.query('SELECT * FROM foo WHERE id = ?', [req.params.id]);
    const relatedBar = await yourDbClient.query('SELECT * FROM bar WHERE foo_id = ?', [oldFoo.id]);

    if (!isValidFooUpdate(req.body, oldFoo, relatedBar)) {
      return res.status(400).json({ error: 'Invalid update request' });
    }

    await yourDbClient.query('UPDATE foo SET ? WHERE id = ?', [req.body, req.params.id]);

    res.status(200).json({ success: true });
  } catch (error) {
    res.status(500).json({ error: (error as Error).message });
  } finally {
    releaseLocks.forEach(release => release());
  }
}

必须注意的关键点

  • 死锁预防:当需要获取多个锁时,一定要对锁键排序后按顺序获取,否则不同请求可能互相等待对方释放锁,导致死锁。
  • 锁的释放:一定要在finally块中释放锁,避免因为代码抛出异常导致锁被永久持有,阻塞后续请求。
  • 适用范围:这两个方案都只适用于单服务器单进程场景,如果未来你的应用扩展到多实例,才需要考虑分布式锁(比如Redis实现的锁),目前你的场景完全不需要。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 10:01:56