如何在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
相关产品推荐
相关产品推荐

