一个请求处理到一半报错,数据库连接关了,Redis 锁却没释放。文件句柄还挂着,直到连接池耗尽、磁盘配额告警,几小时后才有人发现。这类资源泄漏在服务端并不少见,问题往往出在清理链条被上游错误打断。

多数团队会写嵌套的 try-finally,把每个资源包一层。三个资源以内还能看,再多就难读了。更麻烦的是,清理代码本身一旦抛错,后面的资源就不会被释放,释放顺序也变成隐式的、脆弱的。

打开网易新闻 查看精彩图片

一个栈,管住所有资源

TypeScript 的 AsyncDisposableStack 提供了显式的多资源清理协调。它用一个栈追踪每个可释放对象,保证逆序清理,即使某个清理操作失败也不影响其余资源。入口只有一行:await using stack = new AsyncDisposableStack()。

它提供三种注册方式,对应不同场景:

  • adopt():接收一个资源值和一个清理函数,原样返回资源,适合没有实现 Symbol.asyncDispose 的对象,比如归还连接池、释放锁。
  • use():直接注册已实现 AsyncDisposable 的对象,委托给它自己的 Symbol.asyncDispose 实现。
  • defer():把任意异步工作推迟到清理阶段,不绑定具体资源值,适合依赖闭包状态的清理,或无论资源是否分配成功都要执行的操作。

顺序是逆序,错误不丢

清理严格遵循后进先出。先注册数据库连接,再注册 Redis 锁,最后注册文件句柄,释放时就是文件句柄先关、锁第二、连接最后归还。这个顺序有意义:后注册的资源在清理时,往往还需要先注册的资源仍然可用。

清理过程中出现的错误不会互相覆盖,而是聚合成一条 SuppressedError 链,保留每一次失败的上下文,而不是把前面的错误藏起来。

还有一个 move() 方法,把已注册的资源从一个栈整体转移到另一个栈,源栈随即进入已释放状态。这让函数可以构建一组资源,再把清理责任交还给调用方。

栈上还有个 disposed 属性,标记清理序列是否已执行。一旦清理完成,再尝试注册新资源会抛 ReferenceError,防止清理开始后还有人往里塞东西。

请求级资源管理的典型场景

一个处理上传文件的 API 端点能说明问题:写元数据需要数据库事务,防止同一文件并发处理需要 Redis 锁,暂存上传需要临时文件句柄。三个资源必须在请求无论成功还是失败时都原子释放。

AsyncDisposableStack 的价值正体现在这种请求级资源管理里——数据库连接、分布式锁、文件句柄,要么一起释放,要么按确定的顺序释放,不留下半拉子状态。