FlickrProxy #4 - 修正同步上傳的問題

以下為根據文章內容提取並結構化的 15 個問題解決案例。每個案例皆包含問題、根因、解法、程式碼、實測效益與教學要點,適合用於實戰教學、專案練習與能力評估。

Case #1: 同步缺失導致同一照片被上傳兩次

Problem Statement(問題陳述)

業務場景:FlickrProxy 在第一次有人請求某張照片時,會先將該照片上傳到 Flickr,完成後才轉送(redirect)請求至 Flickr 取得圖檔。高併發時,同一張照片可能同時被多人請求。
技術挑戰:第二個請求在第一個上傳未完成前抵達,同樣判定「未建立快取資訊」,導致重複上傳。
影響範圍:浪費頻寬與 API 配額,增加上傳時間,可能造成外部服務的疑慮。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 非原子性的檢查與建立(Check-Then-Act)導致競態條件。
  2. 缺少臨界區保護,並行請求同時執行相同流程。
  3. 建立快取資訊與上傳是分別執行,期間存在時間窗。

深層原因

  • 架構層面:缺少針對共享狀態(快取檔、上傳狀態)的併發控制設計。
  • 技術層面:未使用鎖具保護關鍵區段。
  • 流程層面:未在需求分析時預留高併發測試與保護策略。

Solution Design(解決方案設計)

解決策略:以 lock 將「是否需要上傳」與「建立 Flickr 副本檔」置於同一臨界區,讓同一時間只有一個執行緒可執行該段邏輯,杜絕同一張圖的重複上傳。

實施步驟

  1. 標定關鍵區段
    • 實作細節:鎖住「檢查快取檔是否存在」與「建立快取檔」之間的區段。
    • 所需資源:C# lock、System.IO。
    • 預估時間:0.5 小時。
  2. 導入鎖(初版)
    • 實作細節:以 lock(this.GetType()) 封鎖關鍵區段(做為先行修復)。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  3. 併發測試與驗證
    • 實作細節:並發發送同一張圖的多個 HTTP 請求,確認僅一次上傳。
    • 所需資源:瀏覽器/壓測工具。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 初版:以類型物件作為全域鎖,先阻止重複上傳
lock (this.GetType())
{
    if (!File.Exists(this.CacheInfoFile))
    {
        this.BuildCacheInfoFile(context); // 內含上傳與建立副本
        context.Response.AddHeader("X-FlickrProxy", "Upload");
    }
}

實際案例:FlickrProxy 首次下載觸發上傳,因無鎖導致重複上傳;加上鎖後僅剩一次。
實作環境:ASP.NET(C#)、.NET Framework、IIS。
實測數據:
改善前:同一照片在並發請求下可能上傳 2 次以上。
改善後:同一照片在並發請求下固定只上傳 1 次。
改善幅度:重複上傳率由>0%降至0%。

Learning Points(學習要點) 核心知識點:

  • 競態條件與 Check-Then-Act 的 TOCTOU 問題
  • 臨界區(Critical Section)的設計
  • 以最小變更快速止血的策略

技能要求:

  • 必備技能:C# 基礎、檔案 I/O、lock 用法
  • 進階技能:多執行緒除錯、壓測設計

延伸思考:

  • 若 BuildCacheInfoFile 很耗時,如何縮小鎖範圍?
  • 是否可用檔案系統原子操作(如 CreateNew 模式)做替代?

Practice Exercise(練習題)

  • 基礎練習:撰寫一段會重複建立檔案的並行程式,加入 lock 修復(30 分鐘)。
  • 進階練習:加入壓測,同時 50 個請求,驗證只建立一次(2 小時)。
  • 專案練習:做一個簡易圖片代理,上傳前置與轉送流程含併發控制(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):在並發情境下只上傳一次
  • 程式碼品質(30%):鎖範圍正確、可讀性
  • 效能優化(20%):鎖的範圍與等待時間合理
  • 創新性(10%):提出替代原子化方案(如檔案鎖/原子建立)

Case #2: 鎖太大造成全站序列化(效能嚴重下降)

Problem Statement(問題陳述)

業務場景:FlickrProxy 高峰時段會同時服務多張照片的請求。初步修復以 lock(this.GetType()) 為全域鎖,雖能防重,但任何一張照片上傳時,其他照片請求也被阻擋。
技術挑戰:全站序列化導致多核心 CPU 無法被有效利用,吞吐量下降、延遲上升。
影響範圍:整體併發能力下降,使用者體驗變差。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 使用類型物件作為鎖標的,導致所有請求互斥。
  2. 鎖範圍過大,將不相關的請求也序列化。
  3. 缺乏對「每個資源」獨立併發控制的設計。

深層原因

  • 架構層面:未建立資源層級的鎖粒度策略。
  • 技術層面:錯誤選擇鎖定目標(全域而非資源)。
  • 流程層面:缺少壓測以暴露效能瓶頸。

Solution Design(解決方案設計)

解決策略:改用「每張照片」專屬的鎖物件做資源級互斥,只阻擋同一照片的並發請求,不干擾其他照片。

實施步驟

  1. 定義鎖把手(LockHandle)取得方式
    • 實作細節:為每張照片產生可重用的鎖物件。
    • 所需資源:Dictionary、雜湊鍵。
    • 預估時間:1 小時。
  2. 改寫鎖目標與最小化鎖範圍
    • 實作細節:lock(this.LockHandle) 包住 Check-Then-Act。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  3. 併發與效能驗證
    • 實作細節:同時請求不同照片,觀察是否互不阻塞。
    • 所需資源:壓測工具。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 改為每檔案鎖(資源級鎖)
lock (this.LockHandle)
{
    if (!File.Exists(this.CacheInfoFile))
    {
        this.BuildCacheInfoFile(context);
        context.Response.AddHeader("X-FlickrProxy", "Upload");
    }
}

實際案例:原本一次上傳任何一張圖,都讓其他圖等待;改為每檔案鎖後,不同圖並行服務。
實作環境:ASP.NET(C#)、多核心 CPU。
實測數據:
改善前:不同照片請求彼此阻擋,吞吐量嚴重下降。
改善後:不同照片請求可並行處理,吞吐量顯著提升。
改善幅度:頁面載入時間與吞吐量隨並行度提升(質性改善)。

Learning Points(學習要點)

  • 鎖定粒度的選擇對效能影響巨大
  • 資源級鎖與作業級鎖的差異
  • 並行度與串行化的取捨

技能要求:

  • 必備技能:C# lock、資料結構
  • 進階技能:壓測分析、併發設計

延伸思考:

  • 若檔案數量極多,如何管理鎖物件生命週期?
  • 是否需要公平性以避免飢餓?

Practice Exercise(練習題)

  • 基礎:以字典為不同資源提供鎖,並驗證互不影響(30 分鐘)。
  • 進階:測量每檔案鎖 vs 全域鎖的吞吐差異(2 小時)。
  • 專案:將既有系統的全域鎖重構為資源級鎖(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):不同資源請求不互相阻擋
  • 程式碼品質(30%):鎖的取得與釋放清晰、可維護
  • 效能優化(20%):明顯提高吞吐量
  • 創新性(10%):提出鎖池/WeakReference 管理策略

Case #3: 為每張照片建立專屬鎖物件(LockHandle 設計)

Problem Statement(問題陳述)

業務場景:需要讓「同一張照片」的並發請求互斥,但「不同照片」彼此獨立。
技術挑戰:如何保證同一張照片的請求總能拿到「同一個」鎖物件以互斥?
影響範圍:若鎖物件不一致,將再度發生重複上傳或資料競爭。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 用字串當鎖可能拿到不同實體(即使值相同)。
  2. FileInfo 不是同一實體保證,無法作為共享鎖。
  3. 缺少可重用、可共享的鎖物件註冊機制。

深層原因

  • 架構層面:未建立「資源鍵 → 鎖物件」的目錄。
  • 技術層面:未使用 thread-safe 映射來產生及重用鎖物件。
  • 流程層面:未定義鎖物件生命週期策略。

Solution Design(解決方案設計)

解決策略:以照片內容雜湊(或規範化檔名)作為鍵,建立靜態字典保存鎖物件,確保相同資源共享同一鎖。

實施步驟

  1. 設計 LockHandle 屬性
    • 實作細節:以 MD5/雜湊作鍵,若不存在則建立新 object()。
    • 所需資源:Dictionary<string,object>。
    • 預估時間:1 小時。
  2. 確保 thread-safe 新增
    • 實作細節:lock 字典再 ContainsKey/Add。
    • 所需資源:C# lock。
    • 預估時間:0.5 小時。
  3. 導入與測試
    • 實作細節:並發請求同一張圖,檢查都拿到同一鎖。
    • 所需資源:壓測。
    • 預估時間:1 小時。

關鍵程式碼/設定

private static readonly Dictionary<string, object> _locks = new Dictionary<string, object>();

private object LockHandle
{
    get
    {
        string hash = this.GetFileHash(); // 或對檔名正規化後雜湊
        lock (_locks)
        {
            if (!_locks.ContainsKey(hash))
            {
                _locks.Add(hash, new object()); // 為該資源建立專屬鎖
            }
        }
        return _locks[hash]; // 共用同一鎖物件
    }
}

實際案例:以 MD5 為鍵建立鎖物件字典,確保同資源共享同一把手。
實作環境:ASP.NET、C#。
實測數據:
改善前:同資源可能因不同鎖物件而競態。
改善後:同資源請求必定互斥,不同資源互不干擾。
改善幅度:重複上傳與競態風險趨近 0。

Learning Points(學習要點)

  • 鎖物件「同一性」的重要性
  • 字典型鎖註冊(lock registry)模式
  • 鍵值選擇對正確性的影響

技能要求:

  • 必備技能:C# 集合、字典操作
  • 進階技能:鍵正規化與雜湊碰撞風險評估

延伸思考:

  • 是否需 WeakReference 以避免字典無界成長?
  • 可否用 ConcurrentDictionary 簡化鎖區塊?

Practice Exercise(練習題)

  • 基礎:用字典為「資源鍵」提供鎖物件(30 分鐘)。
  • 進階:將鍵切換為不同策略(檔名、內容雜湊)比較正確性(2 小時)。
  • 專案:封裝為 LockRegistry 類別並寫單元測試(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):同鍵請求共享同一鎖
  • 程式碼品質(30%):封裝良好、可測試
  • 效能優化(20%):鎖查找開銷低
  • 創新性(10%):支援可插拔鍵策略

Case #4: 確保鎖物件註冊的執行緒安全

Problem Statement(問題陳述)

業務場景:多執行緒同時請求 LockHandle,若字典操作非執行緒安全,會出現 KeyNotFound 或重覆新增競態。
技術挑戰:避免 _locks 在 ContainsKey 與 Add 之間被其他執行緒插入。
影響範圍:非預期例外、服務中斷。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 使用非 thread-safe 的 Dictionary。
  2. 未以鎖保護 ContainsKey/Add。
  3. 回傳前未保證條目的存在。

深層原因

  • 架構層面:缺乏共享結構的同步規格。
  • 技術層面:不了解集合的執行緒安全屬性。
  • 流程層面:缺乏並發單元測試。

Solution Design(解決方案設計)

解決策略:以 lock 包覆檢查與新增流程,確保條目存在後再回傳;或改用 ConcurrentDictionary。

實施步驟

  1. 在 LockHandle 中加入字典鎖
    • 實作細節:lock(_locks) { if(!contains) add }。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  2. 壓測與例外監控
    • 實作細節:高併發測試並監控例外。
    • 所需資源:壓測工具。
    • 預估時間:1 小時。

關鍵程式碼/設定

lock (_locks)
{
    if (!_locks.ContainsKey(hash))
    {
        _locks.Add(hash, new object());
    }
}
var handle = _locks[hash]; // 回到鎖外讀取,已保證存在

實際案例:加入鎖後消除因字典競態導致的例外。
實作環境:ASP.NET、C#。
實測數據:
改善前:高併發偶發 KeyNotFound 或 ArgumentException。
改善後:例外歸零。
改善幅度:穩定性大幅提升(質性)。

Learning Points(學習要點)

  • 集合型別的執行緒安全特性
  • 在臨界區保證資料結構不變式
  • 鎖外讀的正確性前提

技能要求:

  • 必備技能:C# lock、Dictionary
  • 進階技能:ConcurrentDictionary 應用

延伸思考:

  • 是否需要雙重檢查(double-checked)與記憶體屏障?
  • 用 Lazy 物件能否簡化?

Practice Exercise(練習題)

  • 基礎:重現無鎖字典的競態,再加鎖修復(30 分鐘)。
  • 進階:改以 ConcurrentDictionary 實作(2 小時)。
  • 專案:封裝 thread-safe registry,提供壓測報告(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):高併發下無例外
  • 程式碼品質(30%):鎖使用正確、簡潔
  • 效能優化(20%):鎖競爭最小化
  • 創新性(10%):替代集合方案

Case #5: 將臨界區縮到最小(只鎖必要的兩個動作)

Problem Statement(問題陳述)

業務場景:為避免重複上傳,需鎖住「判定是否需要上傳」與「建立 Flickr 副本檔」;若鎖範圍再擴大,會拖慢其他請求。
技術挑戰:如何精準界定必須鎖住的最小區段以兼顧正確性與效能。
影響範圍:鎖太大導致效能差;鎖太小造成競態。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 初版鎖範圍過大,序列化無關邏輯。
  2. 若拆錯,結果不正確(再次競態)。
  3. 無明確準則界定關鍵動作。

深層原因

  • 架構層面:缺乏關鍵區段識別與資料不變式定義。
  • 技術層面:臨界區邊界模糊。
  • 流程層面:未經迭代優化鎖範圍。

Solution Design(解決方案設計)

解決策略:鎖只包住「快取狀態決斷」與「建立副本檔」兩步,其他 IO/網路動作盡可能移出鎖外執行。

實施步驟

  1. 梳理流程與資料不變式
    • 實作細節:畫出「檢查→建立→可用」狀態轉換。
    • 所需資源:設計圖。
    • 預估時間:1 小時。
  2. 重構鎖邊界
    • 實作細節:只包住 if (!Exists) { Build… }。
    • 所需資源:C#。
    • 預估時間:1 小時。
  3. 量測效能
    • 實作細節:比較重構前後延遲與吞吐。
    • 所需資源:壓測。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 僅鎖必要動作,縮小臨界區
lock (this.LockHandle)
{
    if (!File.Exists(this.CacheInfoFile))
    {
        this.BuildCacheInfoFile(context); // 建立副本與狀態
        context.Response.AddHeader("X-FlickrProxy", "Upload");
    }
}
// 其他非必要流程(如回應處理)放鎖外

實際案例:只鎖關鍵兩動作後,其他請求延遲下降。
實作環境:ASP.NET、C#。
實測數據:
改善前:鎖範圍大,平均等待時間偏高。
改善後:等待時間下降,吞吐提升。
改善幅度:質性提升,隨請求數成長更穩定。

Learning Points(學習要點)

  • 臨界區縮減與可伸縮性
  • 資料不變式保證
  • 非必要動作移出鎖外

技能要求:

  • 必備技能:流程拆解、C# 同步
  • 進階技能:性能剖析、鎖競爭分析

延伸思考:

  • 網路上傳是否也該移出鎖外並以 semaphore 控制?
  • 是否需要細分「檢查」與「建立」的鎖?

Practice Exercise(練習題)

  • 基礎:將一段大鎖拆成最小臨界區(30 分鐘)。
  • 進階:以 StopWatch 度量等待時間前後差異(2 小時)。
  • 專案:撰寫併發測試覆蓋多種鎖範圍(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):正確避免競態
  • 程式碼品質(30%):鎖邊界清晰
  • 效能優化(20%):可量測改善
  • 創新性(10%):提出替代同步策略

Case #6: 鎖定標的物選型:不要用字串或 FileInfo

Problem Statement(問題陳述)

業務場景:需要為「同一檔案」建立一致的鎖標的物,確保相同資源的請求能互斥。
技術挑戰:字串與 FileInfo 雖代表相同檔案,引用實體卻可能不同。
影響範圍:用錯鎖標的會導致互斥失效、重複上傳。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 字串相同不代表同一 object 實體。
  2. FileInfo 可能多次建立為不同實體,且「不存在」時無法共享。
  3. 未有中心化鎖物件管理。

深層原因

  • 架構層面:缺鎖物件目錄。
  • 技術層面:引用相等與值相等混淆。
  • 流程層面:缺乏併發設計審查。

Solution Design(解決方案設計)

解決策略:以雜湊鍵查表取得同一 object(),避免直接以字串/FileInfo 當鎖標的。

實施步驟

  1. 設計鍵與對映
    • 實作細節:使用內容雜湊或正規化檔名作鍵。
    • 所需資源:字典。
    • 預估時間:1 小時。
  2. 替換鎖標的
    • 實作細節:lock(LockHandle) 取代 lock(字串/ FileInfo)。
    • 所需資源:C#。
    • 預估時間:0.5 小時。

關鍵程式碼/設定

// 錯誤示範(不要這樣)
/*
lock (fileNameString) { ... }
lock (new FileInfo(path)) { ... }
*/
// 正確作法:取用中心化鎖物件
lock (this.LockHandle) { /* 關鍵區段 */ }

實際案例:以中央鎖物件避免以不穩定引用作鎖。
實作環境:ASP.NET、C#。
實測數據:
改善前:偶發互斥失效。
改善後:互斥穩定生效。
改善幅度:正確性提升。

Learning Points(學習要點)

  • 參考相等 vs 值相等
  • 鎖標的物選型原則
  • 中央註冊表模式

技能要求:

  • 必備技能:C# 基礎
  • 進階技能:鍵規範化

延伸思考:

  • 若需跨進程鎖,該如何設計(命名 Mutex/檔案鎖)?

Practice Exercise(練習題)

  • 基礎:展示以字串鎖的問題,改為 LockHandle(30 分鐘)。
  • 進階:設計可插拔鍵提供器(2 小時)。
  • 專案:將專案所有鎖標的一致化(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):同資源互斥生效
  • 程式碼品質(30%):封裝與命名清楚
  • 效能優化(20%):鎖查找低開銷
  • 創新性(10%):鍵策略可擴充

Case #7: 限制同時上傳數量以避免頻寬尖峰(Semaphore)

Problem Statement(問題陳述)

業務場景:同頁面含多張首次載入的圖片,瀏覽器會同時發多個請求,導致同時啟動多個上傳。
技術挑戰:無上限的並行上傳會拖慢整體速度,並可能引起外部服務的關切。
影響範圍:頻寬耗盡、延遲增加、外部 API 風險。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 未限制「上傳」這個昂貴動作的並行度。
  2. 上傳耗時長,造成資源爭用。
  3. 一次大量請求集中引發尖峰。

深層原因

  • 架構層面:缺少併發節流(throttling)層。
  • 技術層面:未使用 Semaphore 控制併發量。
  • 流程層面:缺乏高併發頁面測試。

Solution Design(解決方案設計)

解決策略:使用 Semaphore 將同時上傳數量限制為 2,將昂貴作業序列化到可控程度,提升整體穩定性。

實施步驟

  1. 建立全域 Semaphore
    • 實作細節:new Semaphore(2, 2) 靜態實例。
    • 所需資源:System.Threading。
    • 預估時間:0.5 小時。
  2. 包覆上傳區段
    • 實作細節:WaitOne → 上傳 → Release。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  3. 壓測與調參
    • 實作細節:調整並行度,觀察延遲/吞吐平衡。
    • 所需資源:壓測工具。
    • 預估時間:1.5 小時。

關鍵程式碼/設定

public static Semaphore FlickrUploaderSemaphore = new Semaphore(2, 2);

// 上傳時使用節流
FlickrUploaderSemaphore.WaitOne();
try
{
    photoID = flickr.UploadPicture(this.FileLocation);
}
finally
{
    FlickrUploaderSemaphore.Release();
}

實際案例:同頁多圖首次載入時,並行上傳被限制為 2,避免網路擁塞。
實作環境:ASP.NET、C#。
實測數據:
改善前:同時上傳數量不受控,網路壅塞。
改善後:同時上傳≤2,頁面總完成時間更穩定。
改善幅度:延遲尖峰降低,穩定性顯著提升(質性)。

Learning Points(學習要點)

  • 以 Semaphore 進行併發節流
  • 熱點作業的併發治理
  • try/finally 確保 Release

技能要求:

  • 必備技能:C# 執行緒同步
  • 進階技能:併發調參、效能剖析

延伸思考:

  • 動態調整上限(依負載/時段)?
  • 與排隊策略(Queue)結合?

Practice Exercise(練習題)

  • 基礎:以 Semaphore 限制同一段程式的並發(30 分鐘)。
  • 進階:做併發度=1,2,4 的比較實驗(2 小時)。
  • 專案:為昂貴外呼作業建立可配置的節流層(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):並發上限生效
  • 程式碼品質(30%):正確釋放、無洩漏
  • 效能優化(20%):抑制尖峰
  • 創新性(10%):動態調參策略

Case #8: 同時滿足「同檔互斥」與「總量節流」的組合設計

Problem Statement(問題陳述)

業務場景:要確保同一張照片只上傳一次(正確性),同時限制整體同時上傳數量(穩定性)。
技術挑戰:單用鎖只能解決同檔互斥,單用 Semaphore 只能控總量,兩者需協調組合。
影響範圍:若組合不當,要不是重複上傳,要不就是吞吐不穩定。
複雜度評級:高

Root Cause Analysis(根因分析)

直接原因

  1. 資源級鎖與系統級節流同時存在的協調問題。
  2. 上傳動作應由節流控制而非臨界區長時間佔用。
  3. 缺乏清楚的分層邏輯。

深層原因

  • 架構層面:未將「狀態一致性」與「容量控制」分層。
  • 技術層面:鎖與 Semaphore 的職責界線不清。
  • 流程層面:未以整體路徑進行設計驗證。

Solution Design(解決方案設計)

解決策略:鎖用於原子化的狀態檢查與建立;上傳本身不佔鎖,改由 Semaphore 控制總量,達成正確性與穩定性的平衡。

實施步驟

  1. 劃分責任
    • 實作細節:鎖負責 check/build,Semaphore 負責 upload。
    • 所需資源:設計圖。
    • 預估時間:1 小時。
  2. 串接控制流程
    • 實作細節:先 lock 判定需上傳,再在鎖外 WaitOne/Upload/Release。
    • 所需資源:C#。
    • 預估時間:1 小時。
  3. 併發場景驗證
    • 實作細節:同檔/多檔、大量請求情境。
    • 所需資源:壓測工具。
    • 預估時間:2 小時。

關鍵程式碼/設定

bool needUpload = false;
lock (this.LockHandle)
{
    if (!File.Exists(this.CacheInfoFile))
    {
        needUpload = true; // 僅決策與狀態準備放鎖內
    }
}

if (needUpload)
{
    FlickrUploaderSemaphore.WaitOne();
    try
    {
        // 實際上傳,避免在鎖內執行長任務
        var photoID = flickr.UploadPicture(this.FileLocation);
        // 完成後再更新快取資訊(必要時以細鎖保護)
        this.BuildCacheInfoFile(context);
        context.Response.AddHeader("X-FlickrProxy", "Upload");
    }
    finally
    {
        FlickrUploaderSemaphore.Release();
    }
}

實際案例:同時解決重複上傳與上傳尖峰問題,頁面含多圖時仍表現穩定。
實作環境:ASP.NET、C#。
實測數據:
改善前:僅鎖→吞吐差;僅節流→重複上傳。
改善後:兩者兼顧,誤上傳=0;並發上限生效。
改善幅度:正確性與穩定性顯著提升。

Learning Points(學習要點)

  • 將一致性與容量控制分層
  • 長任務移出鎖、由節流接手
  • 組合同步原語的序列設計

技能要求:

  • 必備技能:lock 與 Semaphore 運用
  • 進階技能:路徑設計與競態驗證

延伸思考:

  • 快取資訊更新是否需二階段提交?
  • 需不需要公平 Semaphore?

Practice Exercise(練習題)

  • 基礎:鎖內只做決策、長任務改由 Semaphore 控制(30 分鐘)。
  • 進階:寫壓測覆蓋多種併發模式(2 小時)。
  • 專案:封裝組合控制器(Consistency + Throttling)(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):同檔互斥與總量節流並存
  • 程式碼品質(30%):分層清晰、責任單一
  • 效能優化(20%):長任務不佔鎖
  • 創新性(10%):可配置與可擴充性

Case #9: 多核心效益釋放:避免不必要的全域互斥

Problem Statement(問題陳述)

業務場景:伺服器已升級多核心 CPU,但全域鎖使請求序列化,硬體效益無法釋放。
技術挑戰:在維持正確性的前提下,提高不同資源請求的並行處理能力。
影響範圍:吞吐量低、CPU 利用率不佳。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 鎖標的過於粗糙(類型物件)。
  2. 不相關的請求彼此互斥。
  3. 長任務佔用鎖資源。

深層原因

  • 架構層面:未配合硬體特性設計併發度。
  • 技術層面:忽略鎖競爭對 CPU 的抑制。
  • 流程層面:缺少硬體升級後的效能回歸測試。

Solution Design(解決方案設計)

解決策略:以每檔案鎖 + 上傳節流的雙層設計,讓不同檔案請求併行、同時限制昂貴操作的併發數。

實施步驟

  1. 改為每檔案鎖
    • 實作細節:使用 LockHandle。
    • 所需資源:字典。
    • 預估時間:1 小時。
  2. 長任務節流
    • 實作細節:上傳由 Semaphore 控制。
    • 所需資源:System.Threading。
    • 預估時間:0.5 小時。
  3. CPU 利用率觀測
    • 實作細節:比較前後 CPU 使用率與 RPS。
    • 所需資源:監控工具。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 參考 Case #2、#7、#8 的組合
// 核心在於:不同檔案請求不互斥,長任務由 Semaphore 控制

實際案例:全域鎖改為資源鎖後,CPU 核心可同時服務不同照片請求。
實作環境:多核心伺服器、ASP.NET。
實測數據:
改善前:CPU 利用率偏低、RPS 停滯。
改善後:CPU 與 RPS 隨並行度增加而提升。
改善幅度:質性提升(依硬體與負載而定)。

Learning Points(學習要點)

  • 鎖競爭與硬體資源利用
  • 架構調整對吞吐的影響
  • 以節流控制昂貴作業

技能要求:

  • 必備技能:性能監控
  • 進階技能:併發調優

延伸思考:

  • 是否需要根據 CPU 核數動態調整 Semaphore 配額?

Practice Exercise(練習題)

  • 基礎:度量全域鎖 vs 資源鎖的 CPU 利用差異(30 分鐘)。
  • 進階:加入節流並比較 RPS(2 小時)。
  • 專案:設計「CPU 感知」的節流器(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):並發提升不破壞正確性
  • 程式碼品質(30%):清楚層次
  • 效能優化(20%):CPU/RPS 改善
  • 創新性(10%):動態調整策略

Case #10: 檔名大小寫問題與鍵選擇:改用內容雜湊(MD5)

Problem Statement(問題陳述)

業務場景:Windows 檔案系統大小寫不敏感,若以檔名作鍵可能出現大小寫異動卻被視為不同鍵或相反。
技術挑戰:確保資源鍵的穩定性與唯一性,減少誤判。
影響範圍:可能導致鎖不一致與重複上傳。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 檔名大小寫與正規化處理不足。
  2. 鍵策略未與平台特性對齊。
  3. 使用值等價但不同引用的鍵。

深層原因

  • 架構層面:鍵策略未標準化。
  • 技術層面:對 FS 行為考慮不足。
  • 流程層面:未定義鍵生成規則。

Solution Design(解決方案設計)

解決策略:使用檔案內容 MD5 作鍵;剛好流程已有 MD5 計算,可直接重用,保證同內容共享鎖。

實施步驟

  1. 實作 GetFileHash
    • 實作細節:回傳 MD5 Hex 字串。
    • 所需資源:加密雜湊 API。
    • 預估時間:1 小時。
  2. 替換鍵策略
    • 實作細節:LockHandle 以 MD5 為鍵。
    • 所需資源:C#。
    • 預估時間:0.5 小時。

關鍵程式碼/設定

string hash = this.GetFileHash(); // 已存在的 MD5 計算結果
lock (_locks)
{
    if (!_locks.ContainsKey(hash))
        _locks.Add(hash, new object());
}
return _locks[hash];

實際案例:文章中說明既有流程已算 MD5,直接拿來當鍵。
實作環境:ASP.NET、C#。
實測數據:
改善前:鍵不穩定可能造成錯誤互斥。
改善後:鍵唯一穩定,互斥正確。
改善幅度:正確性提升。

Learning Points(學習要點)

  • 鍵策略與平台特性
  • 重用既有管線資料(MD5)
  • 內容 vs 名稱 作為鍵

技能要求:

  • 必備技能:雜湊概念
  • 進階技能:鍵碰撞與風險評估

延伸思考:

  • 大檔案 MD5 成本如何?是否可快取?

Practice Exercise(練習題)

  • 基礎:以 MD5 作鍵的 LockRegistry(30 分鐘)。
  • 進階:比較檔名正規化 vs 內容雜湊(2 小時)。
  • 專案:鍵策略可切換與統計(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):鍵穩定唯一
  • 程式碼品質(30%):封裝清晰
  • 效能優化(20%):MD5 開銷受控
  • 創新性(10%):多策略支援

Case #11: 高併發頁面(多圖同頁)下的上傳洪峰抑制

Problem Statement(問題陳述)

業務場景:單頁含多張首次載入的圖片,瀏覽器同時發起多個請求;若每張都觸發上傳,容易造成網路與外部服務壓力。
技術挑戰:既要確保每張圖只上傳一次,又要避免同時上傳過多。
影響範圍:頁面載入過慢、外部 API 關切。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 首次載入大量圖片同時觸發上傳。
  2. 無節流機制。
  3. 上傳動作昂貴。

深層原因

  • 架構層面:無併發尖峰治理方案。
  • 技術層面:缺少 Semaphore 應用。
  • 流程層面:未以真實頁面驗證。

Solution Design(解決方案設計)

解決策略:每檔案鎖保證唯一上傳;跨檔案以 Semaphore 將同時上傳控制在 2。

實施步驟

  1. 每檔案鎖(參考 Case #2/#3)
    • 實作細節:LockHandle。
    • 所需資源:C#。
    • 預估時間:1 小時。
  2. 上傳節流(參考 Case #7)
    • 實作細節:Semaphore(2,2)。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  3. 實頁壓測
    • 實作細節:打開含 20 張圖之頁面,量測總載入時間。
    • 所需資源:瀏覽器 DevTools。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 參考 Case #8 的組合範例,略

實際案例:作者測試同頁多圖時導致大量同時上傳,因而採用 Semaphore 限制為 2。
實作環境:ASP.NET、C#。
實測數據:
改善前:同時上傳過多,載入時間飄忽不定。
改善後:同時上傳≤2,載入時間較穩定。
改善幅度:穩定性明顯提升(質性)。

Learning Points(學習要點)

  • 真實頁面情境的尖峰行為
  • 粒度化鎖 + 全域節流的協同
  • 以使用者體驗為導向的併發治理

技能要求:

  • 必備技能:ASP.NET、C#
  • 進階技能:瀏覽器並行限制理解、壓測

延伸思考:

  • 依瀏覽器/網路環境調整節流上限?
  • 排隊可視化回饋(如回應 header)

Practice Exercise(練習題)

  • 基礎:建立含多圖頁面並觀察行為(30 分鐘)。
  • 進階:加入節流並比較總載入時間(2 小時)。
  • 專案:做一個可配置上限與觀測的上傳管理器(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):穩定控制上傳數量
  • 程式碼品質(30%):結構化清晰
  • 效能優化(20%):載入時間穩定
  • 創新性(10%):自適應上限

Case #12: 以回應標頭標記上傳事件,提升可觀測性

Problem Statement(問題陳述)

業務場景:需要辨識哪些請求觸發了上傳,以便驗證修復是否生效並做後續監測。
技術挑戰:缺少可觀測性,無法在日誌中快速判定上傳事件。
影響範圍:除錯成本高、無法做趨勢追蹤。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 回應缺少事件標記。
  2. 難以在混雜請求中定位上傳。
  3. 無數據支援決策。

深層原因

  • 架構層面:缺失觀測設計。
  • 技術層面:未利用 Header 進行輕量標記。
  • 流程層面:缺乏營運指標。

Solution Design(解決方案設計)

解決策略:在觸發上傳時加入自訂回應標頭 X-FlickrProxy: Upload,讓代理、前端或日誌系統能快速識別。

實施步驟

  1. 加入標頭
    • 實作細節:context.Response.AddHeader(“X-FlickrProxy”,”Upload”)。
    • 所需資源:ASP.NET。
    • 預估時間:0.3 小時。
  2. 日誌與監測
    • 實作細節:Nginx/IIS 記錄回應標頭,建立統計。
    • 所需資源:日誌系統。
    • 預估時間:1 小時。

關鍵程式碼/設定

context.Response.AddHeader("X-FlickrProxy", "Upload"); // 標記此請求觸發了上傳

實際案例:文章示範在建立快取資訊後加入標頭,便於識別。
實作環境:ASP.NET、IIS。
實測數據:
改善前:無法統計上傳觸發次數。
改善後:可觀測且可統計。
改善幅度:可見性顯著提升。

Learning Points(學習要點)

  • 可觀測性與營運指標
  • 輕量事件標記方法
  • 與監控系統整合

技能要求:

  • 必備技能:HTTP 基礎
  • 進階技能:日誌分析

延伸思考:

  • 是否加入上傳耗時等額外標頭?
  • 以追蹤 ID 串連多層系統?

Practice Exercise(練習題)

  • 基礎:為特定事件加回應標頭(30 分鐘)。
  • 進階:收集並統計標頭事件(2 小時)。
  • 專案:建立上傳監控儀表板(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):標頭正確且可統計
  • 程式碼品質(30%):非侵入、低耦合
  • 效能優化(20%):零或近零開銷
  • 創新性(10%):與追蹤系統串接

Case #13: 單一上傳者(Single Uploader)模式

Problem Statement(問題陳述)

業務場景:同一張照片在同時多請求下,僅允許一個請求去上傳;其他請求需等待並在完成後走快取/轉送。
技術挑戰:如何讓等待者不重複執行上傳,且在完成後能正確回應。
影響範圍:避免重複上傳、改善整體延遲。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 多請求對同一狀態無互斥。
  2. 不知道他人是否已在上傳。
  3. 缺少等待/喚醒機制。

深層原因

  • 架構層面:未定義單一執行者模式。
  • 技術層面:缺鎖或缺少狀態旗標。
  • 流程層面:未規範等待者行為。

Solution Design(解決方案設計)

解決策略:以每檔案鎖實現 Single Uploader。第一個請求判定需上傳並執行;其他請求在鎖外循環檢查快取狀態,待上傳完成後走快取/轉送。

實施步驟

  1. 實作單一上傳決策
    • 實作細節:鎖內判斷與設定狀態。
    • 所需資源:C#。
    • 預估時間:1 小時。
  2. 等待者流向
    • 實作細節:等待者不再上傳,改為輪詢/等待至快取就緒。
    • 所需資源:計時器/短暫睡眠(或事件)。
    • 預估時間:1 小時。

關鍵程式碼/設定

bool needUpload = false;
lock (this.LockHandle)
{
    needUpload = !File.Exists(this.CacheInfoFile);
}
if (needUpload)
{
    // 由此請求負責上傳(可配合 Semaphore)
    // 完成後建立快取資訊
}
else
{
    // 等待者:直接使用既有快取或轉送
}

實際案例:文章中第二個請求導致二次上傳,導入 Single Uploader 後僅一次。
實作環境:ASP.NET、C#。
實測數據:
改善前:同資源多上傳。
改善後:每資源僅一次上傳,其他請求共享成果。
改善幅度:重複上傳降至 0。

Learning Points(學習要點)

  • 單一執行者設計模式
  • 等待者的正確行為
  • 共享結果的使用

技能要求:

  • 必備技能:C# 同步
  • 進階技能:事件/條件變數(若需)

延伸思考:

  • 是否要用事件喚醒機制取代輪詢?

Practice Exercise(練習題)

  • 基礎:實作單一執行者(30 分鐘)。
  • 進階:加入事件通知等待者(2 小時)。
  • 專案:封裝為 SingleFlight 元件(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):同資源僅一次執行
  • 程式碼品質(30%):邏輯清晰
  • 效能優化(20%):等待成本低
  • 創新性(10%):事件化優化

Case #14: 外部服務友善策略:避免「被關切」

Problem Statement(問題陳述)

業務場景:Flickr 可能對異常的短時間大量上傳行為產生關切或限制。
技術挑戰:控制對外服務的即時壓力,避免觸發節流或封鎖。
影響範圍:服務可用性、帳號信譽。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 短時間大量並發上傳。
  2. 無節流或重試間隔策略。
  3. 無用量觀測。

深層原因

  • 架構層面:對外訪問策略缺失。
  • 技術層面:無 Semaphore 或速率限制器。
  • 流程層面:未與外部服務可接受行為對齊。

Solution Design(解決方案設計)

解決策略:以 Semaphore 控並發,必要時加延遲或排隊長度限制,並增加標頭與日誌觀測用量趨勢。

實施步驟

  1. 併發限制
    • 實作細節:Semaphore(2,2)。
    • 所需資源:C#。
    • 預估時間:0.5 小時。
  2. 可觀測性
    • 實作細節:回應標頭與日誌(Case #12)。
    • 所需資源:IIS/日誌。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 同 Case #7,略

實際案例:作者擔憂「被關切」,以並發上限降低瞬時壓力。
實作環境:ASP.NET、C#。
實測數據:
改善前:瞬時大量上傳。
改善後:並發受控,更友善穩定。
改善幅度:風險降低(質性)。

Learning Points(學習要點)

  • 對外服務的友善消費策略
  • 瞬時壓力治理
  • 觀測與治理閉環

技能要求:

  • 必備技能:同步原語
  • 進階技能:用量監控

延伸思考:

  • 速率限制(Rate Limiting)是否更合適?
  • 異常/重試策略如何設計?

Practice Exercise(練習題)

  • 基礎:限制對外呼叫並發(30 分鐘)。
  • 進階:加入失敗重試間隔(2 小時)。
  • 專案:設計對外呼叫治理元件(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):壓力受控
  • 程式碼品質(30%):可配置
  • 效能優化(20%):尖峰降低
  • 創新性(10%):治理策略

Case #15: 競態修復的端到端測試與壓測腳本設計

Problem Statement(問題陳述)

業務場景:需證明修復有效(無重複上傳)且效能可接受(多圖併發時穩定)。
技術挑戰:設計可重現並驗證競態與尖峰的測試場景。
影響範圍:品質保證、回歸風險。
複雜度評級:中

Root Cause Analysis(根因分析)

直接原因

  1. 缺乏針對併發場景的自動化測試。
  2. 無壓測數據支撐調整。
  3. 變更後缺回歸驗證。

深層原因

  • 架構層面:未建立可測性設計。
  • 技術層面:缺壓測腳本。
  • 流程層面:缺回歸流程。

Solution Design(解決方案設計)

解決策略:建立兩類測試:同資源並發請求測「單一上傳」;多資源併發測「吞吐與節流」。串接指標(回應標頭)做驗證。

實施步驟

  1. 同資源競態測試
    • 實作細節:同一 URL 發出 N 個請求,確認只上傳一次。
    • 所需資源:JMeter/k6。
    • 預估時間:1.5 小時。
  2. 多資源尖峰測試
    • 實作細節:20 張首次載入圖片同時請求,觀察總載入時間。
    • 所需資源:JMeter/k6、瀏覽器。
    • 預估時間:1.5 小時。
  3. 指標驗證
    • 實作細節:統計 X-FlickrProxy 標頭次數。
    • 所需資源:日誌系統。
    • 預估時間:1 小時。

關鍵程式碼/設定

// 測試輔助:檢查回應是否帶有 X-FlickrProxy: Upload
// 以此統計實際觸發上傳的請求數量

實際案例:以同頁多圖重現問題,再以節流與資源鎖修復並實測。
實作環境:ASP.NET、C#、JMeter/k6。
實測數據:
改善前:重複上傳、多圖尖峰導致延遲不穩。
改善後:重複上傳=0;延遲曲線平滑。
改善幅度:穩定性與正確性提升。

Learning Points(學習要點)

  • 併發測試設計
  • 指標導向驗證
  • 端到端品質保障

技能要求:

  • 必備技能:壓測工具
  • 進階技能:指標分析

延伸思考:

  • 將測試納入 CI 做回歸?
  • 以混沌測試驗證韌性?

Practice Exercise(練習題)

  • 基礎:寫同資源並發的小壓測(30 分鐘)。
  • 進階:寫多資源尖峰測試(2 小時)。
  • 專案:建立 CI 壓測流水線(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):測試覆蓋關鍵場景
  • 程式碼品質(30%):腳本清晰、可維護
  • 效能優化(20%):能發現瓶頸
  • 創新性(10%):自動化與報表化

Case #16: 鎖物件生命週期與記憶體治理(以程序生命週期為界)

Problem Statement(問題陳述)

業務場景:鎖物件被字典持有,長時間運行是否會造成記憶體負擔?
技術挑戰:在確保鎖一致性的前提下,控制鎖物件的生命週期。
影響範圍:記憶體佔用與可維運性。
複雜度評級:低

Root Cause Analysis(根因分析)

直接原因

  1. 鎖物件會持續保留在靜態字典中。
  2. 沒有回收機制。
  3. 長時間運行資源數增加。

深層原因

  • 架構層面:生命週期策略未定義。
  • 技術層面:缺少清理流程或弱引用。
  • 現場面:文章中採用「程序生命週期足矣」的取捨。

Solution Design(解決方案設計)

解決策略:採用程序生命週期的取捨(文章所述),短期以簡單為先;若資源量成長,再引入清理策略或 WeakReference。

實施步驟

  1. 先行採用靜態字典
    • 實作細節:簡單穩定,最小化複雜度。
    • 所需資源:C#。
    • 預估時間:0.2 小時。
  2. 監控與門檻
    • 實作細節:觀測字典項目數,超門檻再治理。
    • 所需資源:監控。
    • 預估時間:1 小時。

關鍵程式碼/設定

private static Dictionary<string, object> _locks = new Dictionary<string, object>();
// 以程序生命週期為界,不主動回收;必要時再優化

實際案例:作者表明「在此程序有生之年,同一檔案拿到同一 object 就足夠」。
實作環境:ASP.NET、C#。
實測數據:
改善前:擔心記憶體占用。
改善後:用簡單策略先達成正確性與效能,再視情況優化。
改善幅度:複雜度顯著降低。

Learning Points(學習要點)

  • 工程權衡:簡單先行 vs 提前優化
  • 生命週期管理策略
  • 觀測驅動的優化

技能要求:

  • 必備技能:C# 靜態資源
  • 進階技能:WeakReference/清理策略設計

延伸思考:

  • 若是長時間大量新資源,應如何回收?
  • 是否需以 LRU 清理?

Practice Exercise(練習題)

  • 基礎:實作靜態鎖表並監控大小(30 分鐘)。
  • 進階:加入 WeakReference 清理策略(2 小時)。
  • 專案:做可配置的鎖表管理器(8 小時)。

Assessment Criteria(評估標準)

  • 功能完整性(40%):正確共享鎖
  • 程式碼品質(30%):清晰、可擴充
  • 效能優化(20%):無明顯記憶體增長
  • 創新性(10%):清理策略設計

案例分類

1) 按難度分類

  • 入門級(適合初學者):Case 4, 6, 10, 12, 16
  • 中級(需要一定基礎):Case 1, 2, 3, 5, 7, 11, 15
  • 高級(需要深厚經驗):Case 8, 9, 14

2) 按技術領域分類

  • 架構設計類:Case 2, 3, 5, 8, 9, 10, 14, 16
  • 效能優化類:Case 2, 5, 7, 8, 9, 11, 15
  • 整合開發類:Case 7, 8, 11, 12, 14
  • 除錯診斷類:Case 1, 4, 6, 12, 15
  • 安全防護類:無(本文聚焦併發與效能)

3) 按學習目標分類

  • 概念理解型:Case 4, 6, 10, 12, 16
  • 技能練習型:Case 1, 2, 3, 5, 7
  • 問題解決型:Case 8, 9, 11, 14, 15
  • 創新應用型:Case 8, 14, 16

案例關聯圖(學習路徑建議)

  • 建議先學:Case 1(競態與臨界區基礎)、Case 12(可觀測性),建立問題意識與觀測手段。
  • 進一步:Case 2(避免全域鎖)→ Case 3/4/6/10(每檔案鎖與鍵策略、執行緒安全)→ Case 5(縮小臨界區)。
  • 併發治理:Case 7(Semaphore 節流)→ Case 11(多圖頁面實戰)→ Case 9(多核心效益)。
  • 組合設計:Case 8(同檔互斥 + 總量節流整合)為核心里程碑。
  • 營運友善:Case 14(外部服務友善)→ Case 16(生命週期治理)。
  • 測試驗證:最後以 Case 15 建立 E2E 壓測與回歸機制,形成完整閉環。

依賴關係提示:

  • Case 8 依賴 Case 2/3/5/7 的知識。
  • Case 11 依賴 Case 7 與 Case 2/3。
  • Case 9 的效益驗證依賴 Case 2 的改造。
  • Case 15 需結合 Case 12 的可觀測性。

完整學習路徑總結: 1) Case 1 → 12 → 2 → 3 → 4 → 6 → 10 → 5
2) Case 7 → 11 → 9 → 8
3) Case 14 → 16 → 15

完成上述路徑後,學習者可掌握從競態診斷、鎖粒度設計、鍵策略、節流治理,到整合實戰與觀測驗證的完整技能組合。






Facebook Pages

AI Synthesis Contents

- 原始文章內容
- 問答集
- 文章摘要
- 解決方案 / Case Study

Edit Post (Pull Request)

Post Directory