前情提要
在 code review 中 RD 寫了以下的程式
1 | if (!await this.exists(fullPath)) { |
看似合理的「檢查然後執行」(Check-Then-Act)模式可能導致的併發問題。
假設兩個請求同時上傳檔案到同一個不存在的目錄:
1 | 時間線: |
雖然使用了 recursive: true,那前面的檢查很可能是不必要的行為。
為什麼會這樣?
問題的根源在於時間窗口。兩個步驟之間存在時間差,而這個時間差就是競態條件的溫床:
1 | // ❌ 有時間窗口的寫法 |
解決方案
方法1: 直接使用 mkdir(推薦)
1 | async uploadFile(file: UploadedFile, pathToStore?: `/${string}`): Promise<string> { |
為什麼 recursive: true 已經足夠
根據 Node.js 官方文件,fs.promises.mkdir(path, { recursive: true }) 具有以下特性:
- 自動建立父目錄:如果父目錄不存在會自動建立
- 處理已存在目錄:當
recursive為true時,如果目錄已存在不會拋出錯誤 - 簡化錯誤處理:避免了手動檢查目錄是否存在的需要
官方範例:
1 | import { mkdir } from 'node:fs'; |
效能優勢
修復後還有意外的效能提升:
1 | // 修復前:2次系統調用 |
經驗教訓
- 避免 Check-Then-Act 模式:這是併發程式設計的經典陷阱,不過這次案例,執行的底層實作已處理好,所以不會有問題
- 信任系統調用:現代 API 通常已經考慮了併發場景
- 簡單就是美:移除不必要的檢查邏輯,程式碼更簡潔也更安全
參考
(fin)