以下為根據文章內容提取並結構化的 15 個問題解決案例。每個案例皆包含問題、根因、解法、程式碼、實測效益與教學要點,適合用於實戰教學、專案練習與能力評估。
Case #1: 同步缺失導致同一照片被上傳兩次
Problem Statement(問題陳述)
業務場景:FlickrProxy 在第一次有人請求某張照片時,會先將該照片上傳到 Flickr,完成後才轉送(redirect)請求至 Flickr 取得圖檔。高併發時,同一張照片可能同時被多人請求。
技術挑戰:第二個請求在第一個上傳未完成前抵達,同樣判定「未建立快取資訊」,導致重複上傳。
影響範圍:浪費頻寬與 API 配額,增加上傳時間,可能造成外部服務的疑慮。
複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 非原子性的檢查與建立(Check-Then-Act)導致競態條件。
- 缺少臨界區保護,並行請求同時執行相同流程。
- 建立快取資訊與上傳是分別執行,期間存在時間窗。
深層原因:
- 架構層面:缺少針對共享狀態(快取檔、上傳狀態)的併發控制設計。
- 技術層面:未使用鎖具保護關鍵區段。
- 流程層面:未在需求分析時預留高併發測試與保護策略。
Solution Design(解決方案設計)
解決策略:以 lock 將「是否需要上傳」與「建立 Flickr 副本檔」置於同一臨界區,讓同一時間只有一個執行緒可執行該段邏輯,杜絕同一張圖的重複上傳。
實施步驟:
- 標定關鍵區段
- 實作細節:鎖住「檢查快取檔是否存在」與「建立快取檔」之間的區段。
- 所需資源:C# lock、System.IO。
- 預估時間:0.5 小時。
- 導入鎖(初版)
- 實作細節:以 lock(this.GetType()) 封鎖關鍵區段(做為先行修復)。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 併發測試與驗證
- 實作細節:並發發送同一張圖的多個 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(根因分析)
直接原因:
- 使用類型物件作為鎖標的,導致所有請求互斥。
- 鎖範圍過大,將不相關的請求也序列化。
- 缺乏對「每個資源」獨立併發控制的設計。
深層原因:
- 架構層面:未建立資源層級的鎖粒度策略。
- 技術層面:錯誤選擇鎖定目標(全域而非資源)。
- 流程層面:缺少壓測以暴露效能瓶頸。
Solution Design(解決方案設計)
解決策略:改用「每張照片」專屬的鎖物件做資源級互斥,只阻擋同一照片的並發請求,不干擾其他照片。
實施步驟:
- 定義鎖把手(LockHandle)取得方式
- 實作細節:為每張照片產生可重用的鎖物件。
- 所需資源:Dictionary、雜湊鍵。
- 預估時間:1 小時。
- 改寫鎖目標與最小化鎖範圍
- 實作細節:lock(this.LockHandle) 包住 Check-Then-Act。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 併發與效能驗證
- 實作細節:同時請求不同照片,觀察是否互不阻塞。
- 所需資源:壓測工具。
- 預估時間: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(根因分析)
直接原因:
- 用字串當鎖可能拿到不同實體(即使值相同)。
- FileInfo 不是同一實體保證,無法作為共享鎖。
- 缺少可重用、可共享的鎖物件註冊機制。
深層原因:
- 架構層面:未建立「資源鍵 → 鎖物件」的目錄。
- 技術層面:未使用 thread-safe 映射來產生及重用鎖物件。
- 流程層面:未定義鎖物件生命週期策略。
Solution Design(解決方案設計)
解決策略:以照片內容雜湊(或規範化檔名)作為鍵,建立靜態字典保存鎖物件,確保相同資源共享同一鎖。
實施步驟:
- 設計 LockHandle 屬性
- 實作細節:以 MD5/雜湊作鍵,若不存在則建立新 object()。
- 所需資源:Dictionary<string,object>。
- 預估時間:1 小時。
- 確保 thread-safe 新增
- 實作細節:lock 字典再 ContainsKey/Add。
- 所需資源:C# lock。
- 預估時間:0.5 小時。
- 導入與測試
- 實作細節:並發請求同一張圖,檢查都拿到同一鎖。
- 所需資源:壓測。
- 預估時間: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(根因分析)
直接原因:
- 使用非 thread-safe 的 Dictionary。
- 未以鎖保護 ContainsKey/Add。
- 回傳前未保證條目的存在。
深層原因:
- 架構層面:缺乏共享結構的同步規格。
- 技術層面:不了解集合的執行緒安全屬性。
- 流程層面:缺乏並發單元測試。
Solution Design(解決方案設計)
解決策略:以 lock 包覆檢查與新增流程,確保條目存在後再回傳;或改用 ConcurrentDictionary。
實施步驟:
- 在 LockHandle 中加入字典鎖
- 實作細節:lock(_locks) { if(!contains) add }。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 壓測與例外監控
- 實作細節:高併發測試並監控例外。
- 所需資源:壓測工具。
- 預估時間: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(根因分析)
直接原因:
- 初版鎖範圍過大,序列化無關邏輯。
- 若拆錯,結果不正確(再次競態)。
- 無明確準則界定關鍵動作。
深層原因:
- 架構層面:缺乏關鍵區段識別與資料不變式定義。
- 技術層面:臨界區邊界模糊。
- 流程層面:未經迭代優化鎖範圍。
Solution Design(解決方案設計)
解決策略:鎖只包住「快取狀態決斷」與「建立副本檔」兩步,其他 IO/網路動作盡可能移出鎖外執行。
實施步驟:
- 梳理流程與資料不變式
- 實作細節:畫出「檢查→建立→可用」狀態轉換。
- 所需資源:設計圖。
- 預估時間:1 小時。
- 重構鎖邊界
- 實作細節:只包住 if (!Exists) { Build… }。
- 所需資源:C#。
- 預估時間:1 小時。
- 量測效能
- 實作細節:比較重構前後延遲與吞吐。
- 所需資源:壓測。
- 預估時間: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(根因分析)
直接原因:
- 字串相同不代表同一 object 實體。
- FileInfo 可能多次建立為不同實體,且「不存在」時無法共享。
- 未有中心化鎖物件管理。
深層原因:
- 架構層面:缺鎖物件目錄。
- 技術層面:引用相等與值相等混淆。
- 流程層面:缺乏併發設計審查。
Solution Design(解決方案設計)
解決策略:以雜湊鍵查表取得同一 object(),避免直接以字串/FileInfo 當鎖標的。
實施步驟:
- 設計鍵與對映
- 實作細節:使用內容雜湊或正規化檔名作鍵。
- 所需資源:字典。
- 預估時間:1 小時。
- 替換鎖標的
- 實作細節: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(根因分析)
直接原因:
- 未限制「上傳」這個昂貴動作的並行度。
- 上傳耗時長,造成資源爭用。
- 一次大量請求集中引發尖峰。
深層原因:
- 架構層面:缺少併發節流(throttling)層。
- 技術層面:未使用 Semaphore 控制併發量。
- 流程層面:缺乏高併發頁面測試。
Solution Design(解決方案設計)
解決策略:使用 Semaphore 將同時上傳數量限制為 2,將昂貴作業序列化到可控程度,提升整體穩定性。
實施步驟:
- 建立全域 Semaphore
- 實作細節:new Semaphore(2, 2) 靜態實例。
- 所需資源:System.Threading。
- 預估時間:0.5 小時。
- 包覆上傳區段
- 實作細節:WaitOne → 上傳 → Release。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 壓測與調參
- 實作細節:調整並行度,觀察延遲/吞吐平衡。
- 所需資源:壓測工具。
- 預估時間: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(根因分析)
直接原因:
- 資源級鎖與系統級節流同時存在的協調問題。
- 上傳動作應由節流控制而非臨界區長時間佔用。
- 缺乏清楚的分層邏輯。
深層原因:
- 架構層面:未將「狀態一致性」與「容量控制」分層。
- 技術層面:鎖與 Semaphore 的職責界線不清。
- 流程層面:未以整體路徑進行設計驗證。
Solution Design(解決方案設計)
解決策略:鎖用於原子化的狀態檢查與建立;上傳本身不佔鎖,改由 Semaphore 控制總量,達成正確性與穩定性的平衡。
實施步驟:
- 劃分責任
- 實作細節:鎖負責 check/build,Semaphore 負責 upload。
- 所需資源:設計圖。
- 預估時間:1 小時。
- 串接控制流程
- 實作細節:先 lock 判定需上傳,再在鎖外 WaitOne/Upload/Release。
- 所需資源:C#。
- 預估時間:1 小時。
- 併發場景驗證
- 實作細節:同檔/多檔、大量請求情境。
- 所需資源:壓測工具。
- 預估時間: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(根因分析)
直接原因:
- 鎖標的過於粗糙(類型物件)。
- 不相關的請求彼此互斥。
- 長任務佔用鎖資源。
深層原因:
- 架構層面:未配合硬體特性設計併發度。
- 技術層面:忽略鎖競爭對 CPU 的抑制。
- 流程層面:缺少硬體升級後的效能回歸測試。
Solution Design(解決方案設計)
解決策略:以每檔案鎖 + 上傳節流的雙層設計,讓不同檔案請求併行、同時限制昂貴操作的併發數。
實施步驟:
- 改為每檔案鎖
- 實作細節:使用 LockHandle。
- 所需資源:字典。
- 預估時間:1 小時。
- 長任務節流
- 實作細節:上傳由 Semaphore 控制。
- 所需資源:System.Threading。
- 預估時間:0.5 小時。
- 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(根因分析)
直接原因:
- 檔名大小寫與正規化處理不足。
- 鍵策略未與平台特性對齊。
- 使用值等價但不同引用的鍵。
深層原因:
- 架構層面:鍵策略未標準化。
- 技術層面:對 FS 行為考慮不足。
- 流程層面:未定義鍵生成規則。
Solution Design(解決方案設計)
解決策略:使用檔案內容 MD5 作鍵;剛好流程已有 MD5 計算,可直接重用,保證同內容共享鎖。
實施步驟:
- 實作 GetFileHash
- 實作細節:回傳 MD5 Hex 字串。
- 所需資源:加密雜湊 API。
- 預估時間:1 小時。
- 替換鍵策略
- 實作細節: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(根因分析)
直接原因:
- 首次載入大量圖片同時觸發上傳。
- 無節流機制。
- 上傳動作昂貴。
深層原因:
- 架構層面:無併發尖峰治理方案。
- 技術層面:缺少 Semaphore 應用。
- 流程層面:未以真實頁面驗證。
Solution Design(解決方案設計)
解決策略:每檔案鎖保證唯一上傳;跨檔案以 Semaphore 將同時上傳控制在 2。
實施步驟:
- 每檔案鎖(參考 Case #2/#3)
- 實作細節:LockHandle。
- 所需資源:C#。
- 預估時間:1 小時。
- 上傳節流(參考 Case #7)
- 實作細節:Semaphore(2,2)。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 實頁壓測
- 實作細節:打開含 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(根因分析)
直接原因:
- 回應缺少事件標記。
- 難以在混雜請求中定位上傳。
- 無數據支援決策。
深層原因:
- 架構層面:缺失觀測設計。
- 技術層面:未利用 Header 進行輕量標記。
- 流程層面:缺乏營運指標。
Solution Design(解決方案設計)
解決策略:在觸發上傳時加入自訂回應標頭 X-FlickrProxy: Upload,讓代理、前端或日誌系統能快速識別。
實施步驟:
- 加入標頭
- 實作細節:context.Response.AddHeader(“X-FlickrProxy”,”Upload”)。
- 所需資源:ASP.NET。
- 預估時間:0.3 小時。
- 日誌與監測
- 實作細節: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(根因分析)
直接原因:
- 多請求對同一狀態無互斥。
- 不知道他人是否已在上傳。
- 缺少等待/喚醒機制。
深層原因:
- 架構層面:未定義單一執行者模式。
- 技術層面:缺鎖或缺少狀態旗標。
- 流程層面:未規範等待者行為。
Solution Design(解決方案設計)
解決策略:以每檔案鎖實現 Single Uploader。第一個請求判定需上傳並執行;其他請求在鎖外循環檢查快取狀態,待上傳完成後走快取/轉送。
實施步驟:
- 實作單一上傳決策
- 實作細節:鎖內判斷與設定狀態。
- 所需資源:C#。
- 預估時間:1 小時。
- 等待者流向
- 實作細節:等待者不再上傳,改為輪詢/等待至快取就緒。
- 所需資源:計時器/短暫睡眠(或事件)。
- 預估時間: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(根因分析)
直接原因:
- 短時間大量並發上傳。
- 無節流或重試間隔策略。
- 無用量觀測。
深層原因:
- 架構層面:對外訪問策略缺失。
- 技術層面:無 Semaphore 或速率限制器。
- 流程層面:未與外部服務可接受行為對齊。
Solution Design(解決方案設計)
解決策略:以 Semaphore 控並發,必要時加延遲或排隊長度限制,並增加標頭與日誌觀測用量趨勢。
實施步驟:
- 併發限制
- 實作細節:Semaphore(2,2)。
- 所需資源:C#。
- 預估時間:0.5 小時。
- 可觀測性
- 實作細節:回應標頭與日誌(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(根因分析)
直接原因:
- 缺乏針對併發場景的自動化測試。
- 無壓測數據支撐調整。
- 變更後缺回歸驗證。
深層原因:
- 架構層面:未建立可測性設計。
- 技術層面:缺壓測腳本。
- 流程層面:缺回歸流程。
Solution Design(解決方案設計)
解決策略:建立兩類測試:同資源並發請求測「單一上傳」;多資源併發測「吞吐與節流」。串接指標(回應標頭)做驗證。
實施步驟:
- 同資源競態測試
- 實作細節:同一 URL 發出 N 個請求,確認只上傳一次。
- 所需資源:JMeter/k6。
- 預估時間:1.5 小時。
- 多資源尖峰測試
- 實作細節:20 張首次載入圖片同時請求,觀察總載入時間。
- 所需資源:JMeter/k6、瀏覽器。
- 預估時間:1.5 小時。
- 指標驗證
- 實作細節:統計 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(根因分析)
直接原因:
- 鎖物件會持續保留在靜態字典中。
- 沒有回收機制。
- 長時間運行資源數增加。
深層原因:
- 架構層面:生命週期策略未定義。
- 技術層面:缺少清理流程或弱引用。
- 現場面:文章中採用「程序生命週期足矣」的取捨。
Solution Design(解決方案設計)
解決策略:採用程序生命週期的取捨(文章所述),短期以簡單為先;若資源量成長,再引入清理策略或 WeakReference。
實施步驟:
- 先行採用靜態字典
- 實作細節:簡單穩定,最小化複雜度。
- 所需資源:C#。
- 預估時間:0.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
完成上述路徑後,學習者可掌握從競態診斷、鎖粒度設計、鍵策略、節流治理,到整合實戰與觀測驗證的完整技能組合。