架構面試題 #1, 線上交易的正確性
問題與答案 (FAQ)
Q&A 類別 A: 概念理解類
A-Q1: 什麼是「線上交易的正確性」?
- A簡: 指在任何執行情境下,交易結果與期望一致,金額不憑空增減,保持系統內外金錢守恆。
- A詳: 線上交易的正確性強調在並發、失敗重試與不同部署規模下,交易執行的結果應與業務期望一致。具體包含:不多扣、不少扣、不重複、無遺漏;在封閉系統內維持總額守恆(除非外部流入/流出);操作具原子性與一致性。文章用「N 執行緒 × M 次,每次 +1 元」驗證,期望最終餘額精準等於原額加總交易金額。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q2, A-Q3, B-Q9
A-Q2: 什麼是「金錢守恆定律」?
- A簡: 在封閉系統內,錢不會憑空產生或消失,只允許在帳戶間轉移且總和不變。
- A詳: 金錢守恆定律是指只要沒有外部資金的流入與流出,系統內的資金總量必須固定。其意涵包括:交易不能只做一半(單邊成功);所有交易步驟須完整落實;對帳時應能證明「期初 + 流入 − 流出 = 期末」。這是衡量交易正確性的核心檢核點,也是設計與測試(如並發壓測)時的驗證依據。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q1, B-Q7, C-Q2
A-Q3: 為何交易「不能只做一半」?
- A簡: 只做一半會破壞守恆與一致性,造成資金遺失或重複,導致帳務失準。
- A詳: 交易通常包含多步驟(如寫入交易紀錄與更新餘額)。若任一步驟失敗或被並發打斷,僅部分生效,會出現「扣款成功、入帳失敗」或反之的情況,破壞守恆與一致性。解方是以原子性執行這些步驟:單機用臨界區(lock),資料庫以 ACID 交易,分散式用可靠的分散式鎖來保護臨界操作。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q1, B-Q3, B-Q5
A-Q4: 什麼是「臨界區」(Critical Section)?
- A簡: 一段不得被並行打斷的程式區域,用鎖保護以避免競態造成錯誤。
- A詳: 臨界區是指會讀寫共享狀態(如帳戶餘額、交易歷史)的程式區段。若多執行緒同時進入,易發生讀寫交錯,導致狀態不一致。透過 lock(或 Interlocked/原子指令),保證一次僅一個執行緒進入,完成讀取、計算、寫回整個步驟後再放行,確保交易的原子性與正確性。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q5, B-Q1, C-Q1
A-Q5: 什麼是「競態條件」(Race Condition)?
- A簡: 多個執行緒同時讀寫共享資源,因時序交錯導致結果不穩定或錯誤。
- A詳: 競態條件常見於並行更新場景,例如兩執行緒同時讀取相同餘額後各自加 1,再寫回,最後只加一次。原因是「讀取→計算→寫回」不是原子操作,時序交錯導致覆蓋。須以鎖、原子指令或 ACID 交易等機制將整段更新步驟包成不可分割的臨界操作,消除競態。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q2, C-Q1, D-Q2
A-Q6: 什麼是「Lock」?為何需要?
- A簡: Lock 是互斥機制,保護臨界區避免並行進入,防止競態與資料錯誤。
- A詳: Lock(互斥鎖)確保同一時間只有一個執行緒進入臨界區,典型用於「讀→改→寫」序列。單機可用語言或 OS 提供的鎖(如 C# lock/Monitor、Interlocked)。多機則需分散式鎖(如 Redis/RedLock)協調不同實例,維持跨程序的一致性。選用與實作正確性直接影響交易安全。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q1, B-Q5, C-Q7
A-Q7: 什麼是 ACID?
- A簡: 資料庫交易的四特性:原子性、一致性、隔離性、持久性,保障交易正確。
- A詳: ACID 定義了可靠交易的基本要求。原子性:交易全成或全不成;一致性:交易前後資料滿足約束與規則;隔離性:並發交易互不干擾,仿如序列執行;持久性:提交後資料不會遺失。文章示範用 SQL Transaction 將「寫交易紀錄+更新餘額」包在同一交易中,讓 DBMS 負責滿足 ACID。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q8, B-Q3, C-Q3
A-Q8: 什麼是資料庫的併發控制(Concurrency Control)?
- A簡: DBMS 為確保 ACID 而採用的鎖定/多版本等機制,管理並發存取衝突。
- A詳: 併發控制透過鎖、隔離層級、或 MVCC 等策略管理多交易同時操作相同資料的衝突,避免髒讀、不可重複讀、幻讀,確保一致性。文章中以 SQL Server 交易示例,DBMS 會自行處理必要鎖與隔離,避免在應用層自行處理複雜的並發細節。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q3, D-Q3, A-Q7
A-Q9: 應用層 Lock 與 SQL 交易的差異?
- A簡: 應用層保護程式內臨界區;SQL 交易由 DBMS 保 ACID,範圍限於單一資料庫。
- A詳: 應用層 Lock(單機或分散式)用於包住多步程式邏輯與多存儲協作;SQL 交易則在資料庫內保證 ACID,適合所有操作都在同一 DB 的情境。當交易跨多服務或多存儲,DB 交易無法全覆蓋,需在應用層以分散式鎖或其他一致性策略完成整體原子性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q5, C-Q7, A-Q18
A-Q10: RDBMS 與 NoSQL 在交易處理上的差異?
- A簡: RDBMS 側重 ACID 與嚴謹結構;NoSQL 側重彈性與可用性,常採最終一致。
- A詳: RDBMS(CA/ACID)擅長嚴格一致、強 schema 的交易;NoSQL 常偏向 BASE(基本可用、軟狀態、最終一致)與彈性結構,原生交易能力較弱(如 Mongo 4.0 以前無多文件交易)。在微服務中,常視情境混用,將需要嚴格一致的部分交給 RDBMS,或用分散式鎖等方式在 NoSQL 上維持正確性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q19, B-Q12, D-Q7
A-Q11: 為何微服務下仍須嚴格交易控制?
- A簡: 金流不容錯,服務再多也須維持守恆與一致,否則系統信任崩壞。
- A詳: 微服務拆分帶來獨立伸縮,但資金流動跨服務時仍必須符合守恆與一致。若缺乏嚴格控制,將造成對帳困難、損益不實、商業風險倍增。可依交易範圍採局部 ACID(單 DB)、應用層分散式鎖、或其他一致性策略,確保跨服務交易的原子性與可追溯性(交易歷史)。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q5, A-Q23, C-Q7
A-Q12: 什麼是分散式鎖(Distributed Lock)?
- A簡: 跨多主機/程序協調互斥的機制,用共享儲存協定化鎖定,保護臨界區。
- A詳: 當同一資源可能被多實例同時更新(如多服務節點更新同帳戶),需要跨程序互斥。分散式鎖常藉助共享且高可用的儲存(如 Redis)實作,核心包含鎖資源鍵、租約(到期時間)、重試等待,確保只有持有鎖者能進入臨界區並在故障時自動釋放。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q5, B-Q6, C-Q7
A-Q13: 什麼是 RedLock?文中如何使用?
- A簡: RedLock 是基於 Redis 的分散式鎖算法/實作,文中以 RedLock.net 取得互斥。
- A詳: RedLock 透過在 Redis 上以資源鍵設置鎖、指定租約到期(expiry)、等待與重試策略(wait/retry)取得鎖。文章示例以 RedLockFactory 建立鎖,進入鎖內執行「查找/新增帳戶、更新餘額、寫入交易紀錄」後釋放,確保多實例對同帳戶更新的臨界區互斥。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q6, C-Q7, D-Q4
A-Q14: 為什麼以「N 執行緒 × M 次 × +1 元」驗證正確性?
- A簡: 簡單可控、可重現並發衝突,期望值明確,適合自動化單元測試。
- A詳: 每次交易加 1 元,能用「初始餘額 + N×M」直接計算預期結果,快速檢測少扣/多扣。多執行緒能製造競態條件;多程序能驗證分散式鎖。此法同時提供效能觀察(每秒交易數),並以 Expected/Actual 對比判斷 PASS/FAIL,形成穩定的回歸測試。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q9, C-Q2, C-Q10
A-Q15: 金額為何使用 decimal 而非 double?
- A簡: decimal 具精確十進位表示,避免浮點誤差導致金額不準的風險。
- A詳: 金融金額需精確小數表示,double(二進位浮點)在十進位小數常有不可避免的誤差累積,會造成對帳不準。decimal 採十進位定點或高精度表示,能準確處理貨幣運算。文章中 AccountBase 即採 decimal 作為餘額/金額型別,符合金融領域慣例。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: D-Q9, C-Q1, C-Q3
A-Q16: 本地鎖與分散式鎖有何差異?
- A簡: 本地鎖限於單程序;分散式鎖跨程序/主機互斥,需外部共享儲存協調。
- A詳: 本地鎖(如 C# lock)只在同一進程內有效,無法阻止其他程序或主機的並行存取。分散式鎖透過 Redis 等共享節點持有鎖資訊,各節點依協定競逐鎖並遵循租約與重試規則,實現跨程序互斥。規模越大越需分散式鎖以維持一致性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q12, B-Q5, C-Q7
A-Q17: TransactionScope 與 T-SQL 顯式 transaction 的差異?
- A簡: TransactionScope 以程式範圍控制交易;T-SQL 以語句 begin/commit 顯式界定。
- A詳: TransactionScope 提供 .NET 程式碼層級交易邊界,易於包裹多命令;T-SQL 以 begin tran/commit 管控單一連線內交易。若所有操作都在同一資料庫,兩者皆可;若跨資源,TransactionScope 可能引入更複雜的交易協調,須審慎評估。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: C-Q3, C-Q4, A-Q9
A-Q18: 為何 SQL 交易通常限制在單一資料庫?
- A簡: ACID 由單一 DBMS 管控最可靠;跨庫需更複雜協議,成本與風險上升。
- A詳: 單一資料庫可用內建鎖與日誌完整實現 ACID。跨資料庫/資源要維持 ACID,需額外協調與一致性機制,實作與營運風險提升。文章主張當交易跨出單庫邊界,應改在應用層以分散式鎖或其他模式維持整體原子性與守恆。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q9, B-Q3, B-Q5
A-Q19: 何謂 CAP 與 ACID/BASE 的關聯?
- A簡: CAP 描述分散式系統取捨;ACID/BASE對應一致性策略與可用性傾向。
- A詳: CAP 指一致性、可用性、分割容忍不可兼得。RDBMS 偏向 CA 與 ACID;NoSQL 常偏向 AP 與 BASE(基本可用、軟狀態、最終一致)。文章指出在微服務/雲原生場景,依需求選擇合適資料儲存與一致性策略,並用應用層機制補足交易正確性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q10, B-Q12, D-Q7
A-Q20: 為何避免自行實作底層鎖?
- A簡: 鎖的正確性依賴原子原語與嚴謹時序,錯誤代價高,應選可靠套件。
- A詳: 正確的互斥需依賴 CPU/OS 的原子指令(如 compare-and-swap),牽涉時序、故障與時鐘等邊界條件,實作難度與驗證成本高。文章建議採用成熟實作(如 RedLock.net),並理解其原理與限制,再以正確的參數化與監控確保可靠性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q13, C-Q7, D-Q4
A-Q21: 為何在 NoSQL 上仍可保證交易正確?
- A簡: 以分散式鎖把多步操作組成臨界區,確保互斥與原子性,維持守恆。
- A詳: 即便底層儲存不支援多步 ACID 交易,仍可在應用層以分散式鎖保護「讀→改→寫」序列,避免同時更新。同時紀錄交易歷史以供對帳。文章示範用 Mongo(不支援交易的版本)+Redis/RedLock,可靠地完成「更新餘額+寫入歷史」且通過並發與多程序測試。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q12, C-Q7, D-Q5
A-Q22: 鎖的「重試」與「到期」扮演何角色?
- A簡: 重試應對鎖競爭;到期避免持鎖方故障造成永鎖,提升系統健壯性。
- A詳: 當鎖資源繁忙,申請者依策略等待並重試以終獲鎖;持鎖者若崩潰,租約到期自動釋放,避免資源長期被占。文章示例以 wait、retry 與 expiry 參數控制鎖取得行為,平衡吞吐與等待,確保高并發下的正確性與可用性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q6, D-Q4, D-Q6
A-Q23: 單機、10+ 主機、100+ 主機對應策略為何?
- A簡: 單機用語言/OS 鎖;10+ 用 SQL 交易;100+ 用分散式鎖維持互斥與正確。
- A詳: 文章定義了不同規模的基本解:單機以 lock/臨界區消除競態;中型系統把多步更新交由單一 DBMS 的 ACID 交易處理;大規模跨實例環境以分散式鎖(如 Redis/RedLock)協調互斥,再在 NoSQL 或多資料源完成一致更新。抽象化的核心是「保護臨界區」。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q1, B-Q3, B-Q5
A-Q24: 什麼是資料分割/分片在擴展中的角色?
- A簡: 透過水平/垂直切分資料,降低單庫負載,提升可伸縮性與吞吐。
- A詳: 隨流量增長,單一 RDBMS 可能成為瓶頸。可透過垂直分割(依領域拆表/拆庫)或水平分片(依鍵值分散資料)分攤負載。文章指出當需擴展與彈性結構時,NoSQL+應用層一致性(如分散式鎖)常是可行方向。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q10, D-Q7, B-Q22
A-Q25: DBMS 作為單點瓶頸的風險是什麼?
- A簡: 連線/CPU/IO 成為上限,所有交易集中,會限制整體吞吐與可用性。
- A詳: 當交易全委託單一資料庫,並發鎖競爭、連線池、IO 帶寬與寫入日誌能力皆可能限制吞吐。文章示範 SQL 交易吞吐顯著低於單機/分散式鎖方案;擴展時需考慮分割資料、混用 NoSQL、或在應用層維持一致性以分散壓力。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: B-Q17, A-Q23, D-Q7
Q&A 類別 B: 技術原理類
B-Q1: 單機 Lock 解法如何運作?
- A簡: 以互斥鎖包住「寫歷史+加餘額」,保證同時僅一執行緒更新,消除競態。
- A詳: 原理是將「讀→改→寫」組成臨界區,進入前取得 lock,結束後釋放。流程:取得鎖→新增交易紀錄→累加餘額→釋放鎖。核心組件包括:同步物件(_syncroot)、臨界區(lock 區塊)、共享狀態(_balance、_history)。此法在單機內可完全避免交錯寫入、確保金額守恆。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q4, A-Q6, C-Q1
B-Q2: 未加鎖為何會少錢?背後機制是什麼?
- A簡: 同時讀到相同餘額,各自加 1 寫回覆蓋,實際只加一次,屬競態覆蓋。
- A詳: 在「讀→加→寫」非原子時,兩執行緒可能在同一初值上各自計算,最後先寫入者被後寫入者覆蓋。流程:同讀 balance→各自加→最後寫回一次。因沒有互斥,導致丟失更新(lost update)。避免方法:用 lock 封鎖整段操作或採用原子加總的資料結構。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q5, D-Q2, C-Q1
B-Q3: SQL 交易如何保證 ACID?
- A簡: 將多語句包在 begin/commit 內,由 DBMS 鎖與日誌確保原子、一致、持久。
- A詳: 技術原理:交易開始(begin tran)後的多語句在同一交易上下文執行;DBMS 透過鎖與隔離層級避免併發衝突;寫入日誌(WAL)保障崩潰後恢復;提交才持久。流程:begin→insert 交易→update 餘額→select 驗證→commit。核心組件:交易管理器、鎖管理、恢復子系統。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q7, A-Q8, C-Q3
B-Q4: DB 併發控制中「隔離層級」的作用是什麼?
- A簡: 定義並發可見度,平衡一致性與效能,避免髒讀與不可重複讀等問題。
- A詳: 隔離層級(如 Read Committed、Repeatable Read、Serializable)決定交易彼此間讀寫可見性。越高一致性通常代表更多鎖競爭與更低吞吐。選擇恰當的層級可滿足正確性與效能平衡。交易正確性要求避免丟失更新,必要時提高隔離或以應用層鎖輔助。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q8, D-Q3, B-Q17
B-Q5: 分散式鎖如何運作?
- A簡: 以共享儲存記錄鎖持有者與租約,節點據此競逐鎖並遵循到期與重試。
- A詳: 節點嘗試以資源鍵原子設鎖(成功即持有),設定到期避免永鎖;若失敗則等待並重試。持鎖節點在臨界區內執行邏輯,完成即釋放。核心步驟:原子設鎖→執行臨界區→釋放或逾時。組件:共享儲存(Redis)、租約時間、重試策略、資源命名。確保跨程序互斥。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q12, A-Q22, C-Q7
B-Q6: RedLock.NET 的參數(resource、expiry、wait、retry)如何影響行為?
- A簡: resource 定義鎖對象;expiry 租約長度;wait/retry 控制等待與重試節奏。
- A詳: resource 決定互斥粒度(如依帳戶名建立鍵);expiry 太短易過期重入,太長增風險;wait 是最大等待鎖時長,retry 控制輪詢頻率與後退策略。流程:CreateLock(resource, expiry, wait, retry)→IsAcquired→執行→Dispose 釋放。需依交易耗時與並發度調校。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q13, A-Q22, D-Q4
B-Q7: 為何「寫歷史+加餘額」必須原子執行?
- A簡: 任一步驟單獨成功都會破壞守恆或可追溯性,需視為不可分割單元。
- A詳: 交易歷史用於對帳與審計,餘額反映最終狀態。若只更餘額不留歷史,無法追蹤;只留歷史不更餘額,餘額失真。原理是保證兩者要麼全成要麼全不成。單機用鎖包住兩步;DB 用單交易;分散式用鎖確保互斥後再串行執行兩步。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q3, C-Q1, C-Q7
B-Q8: 單機鎖與多機鎖如何映射?
- A簡: 將本地互斥語意投射到共享儲存鎖語意,保護同一臨界區的跨節點互斥。
- A詳: 單機是語言/OS 鎖;多機透過分散式鎖服務維持相同互斥語意。關鍵在一致的資源命名(以帳戶為鍵)、正確選擇租約與重試,確保臨界區在任何節點都排他。程式碼結構可重用:將臨界區抽出,替換鎖實作即可跨規模。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q16, C-Q7, C-Q8
B-Q9: 測試程式如何驗證正確性?
- A簡: 啟動多執行緒同時加值,等待完成後以 Expected/Actual 對比判定 PASS。
- A詳: 流程:建立帳戶引擎→記錄初始餘額→啟動 N 執行緒各執行 M 次加 1→Start/Join 等待全部完成→計算期望值 origin+N×M→讀取實際餘額→比較並輸出 PASS/FAIL 與效能統計。核心組件:Thread、Stopwatch、Join、GetBalance/ExecTransaction。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q14, C-Q2, C-Q10
B-Q10: 多程序測試如何驗證分散式鎖有效?
- A簡: 同機啟多個進程各跑並發加值,最後以外部查詢總額驗證無誤差。
- A詳: 方法:先查初始餘額→以批次腳本啟 10 個進程,每進程 20 執行緒壓測→全部結束後再查最終餘額→比對期望。此法能製造跨程序競爭,驗證分散式鎖是否有效互斥。核心:進程數、執行緒數、外部資料來源(Mongo 查詢)與一致的資源鍵。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: C-Q9, C-Q7, A-Q12
B-Q11: Docker 在實驗環境中的角色?
- A簡: 以容器快速啟動 Redis/Mongo 等依賴,降低部署成本,利於重現測試。
- A詳: 文章用 docker run 一行命令啟動 Redis 與 Mongo 容器,快速提供分散式鎖與 NoSQL 儲存環境。好處:環境一致、可重現、啟停快速,適合 POC 與實驗型壓測。核心指令:映射埠口,指定映像版本(包含 Windows 版)。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: C-Q6, C-Q7, C-Q9
B-Q12: MongoDB 不支援交易時如何保一致?
- A簡: 以分散式鎖把多步更新串行化,避免並發衝突,維持餘額與歷史一致。
- A詳: 在 Mongo 3.x 無多文件交易,文章以 RedLock 保證同帳戶同時僅一個更新在臨界區執行。流程:取鎖→查或建帳戶→更新餘額→寫歷史→釋放鎖。雖無 DB 級 ACID,多程序透過互斥仍可避免丟失更新與交錯寫入,達成實務正確性。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q21, C-Q7, D-Q5
B-Q13: Compare-and-Swap(CAS)是什麼?與鎖的關係?
- A簡: CAS 是不可分割的原子比較交換指令,是實作鎖與原子操作的硬體基礎。
- A詳: CAS 檢查變數是否等於預期值,若相等則原子地交換為新值,否則失敗。OS/語言的鎖、部分無鎖資料結構與原子操作皆依賴此原語。文章提醒臨界區正確性需靠原子原語支撐,若自行實作需理解硬體/時序細節,否則應使用成熟套件。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q20, C-Q1, C-Q7
B-Q14: 重試/退避策略對系統有何影響?
- A簡: 控制鎖競爭時的等待節奏,平衡延遲與吞吐,避免活鎖與過度打擾。
- A詳: 重試頻率太高會造成熱鍵壓力與活鎖,太低則延遲高。可用固定或指數退避,並限制最大等待時間。文章示例以 wait/retry 調節取得鎖的行為,需依交易耗時、競爭程度微調,以提升整體成功率與穩定性。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q22, D-Q4, C-Q8
B-Q15: 鎖資源命名策略如何設計?
- A簡: 使用能唯一標識臨界資源的鍵,如「account-transaction::{UserId}」。
- A詳: 正確的資源鍵定義互斥粒度。以帳戶為粒度可讓不同帳戶並行,同帳戶互斥,最大化吞吐與正確性。命名需穩定、可追蹤、避免衝突,並可考慮加入版本或業務維度。文章示例用「account-transaction::Name」作為鎖鍵。
- 難度: 初級
- 學習階段: 核心
- 關聯概念: C-Q8, A-Q12, D-Q4
B-Q16: 鎖參數選擇如何影響正確與效能?
- A簡: 租約長短影響超時與永鎖風險;等待/重試影響成功率與延遲。
- A詳: 交易耗時波動大時租約要覆蓋 P95 耗時;等待時間不足易失敗,過長則排隊延遲高。重試間隔需避免過熱。需以壓測觀察成功率、延遲與吞吐間的平衡,逐步調校。文章示例以 5s 租約與等待,50ms 重試作為起點。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: B-Q6, B-Q14, D-Q4
B-Q17: 為何 DB 交易吞吐較低?
- A簡: DB 需維持鎖、日誌與隔離,網路往返多,易成瓶頸,吞吐不如應用層鎖。
- A詳: DB 交易為確保 ACID,會進行鎖管理、寫前日誌、沖刷與恢復準備;每次交易涉及多次往返。當並發高時,鎖競爭與 IO 成為限制。文章示範 SQL 交易每秒約數千次,遠低於單機鎖;擴展時需考量分割或遷移部分一致性到應用層。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q25, D-Q7, C-Q3
B-Q18: 為何選 Dapper 而非 EF?
- A簡: Dapper 輕量、直觀、效能佳,範例重心在交易正確,不需完整 ORM。
- A詳: Dapper 為微型 ORM,提供簡潔 SQL 映射與良好效能;EF 提供豐富模型與變更追蹤但較重。文章為示例與聚焦交易邏輯,選 Dapper 降低樣板與學習成本。這樣也能更清楚觀察 SQL 交易語句與行為。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: C-Q3, C-Q4, B-Q3
B-Q19: 並發單元測試如何設計得穩定?
- A簡: 控制執行緒數與迭代、明確 Join、固定輸入,僅檢查可重現的關鍵指標。
- A詳: 測試要具可重現性:固定 N 與 M、統一增量、啟動後立即 Join 等待結束、以 Expected/Actual 判定、輸出吞吐統計供比較。避免加入與排程不穩定相關的外部因素,聚焦臨界區正確性,必要時提高樣本量減少偶發誤差。
- 難度: 初級
- 學習階段: 核心
- 關聯概念: C-Q2, C-Q9, D-Q8
B-Q20: 為何要將交易邏輯集中在單一邏輯邊界?
- A簡: 集中化可定義單一原子單元,利於鎖保護與正確性驗證,減少跨界風險。
- A詳: 若交易步驟分散在多處,難以用單一鎖或 DB 交易保護全流程,易出現只做一半或中途失敗。將「寫歷史+加餘額」集中於單一方法(ExecTransaction)可在不同實作(本地鎖/SQL/分散式鎖)下維持一致原子邏輯,利於測試與擴展。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: C-Q1, C-Q7, A-Q23
B-Q21: 抽象化(AccountBase)帶來什麼好處?
- A簡: 定義穩定介面,替換實作不改測試與上層程式,利於對照與擴充。
- A詳: AccountBase 統一定義 GetBalance/ExecTransaction,使 Lock 版、SQL 版、分散式鎖版可插拔替換。在相同測試下比較正確性與效能。此抽象化隔離技術選型,讓核心業務不受實作細節影響,實作可隨規模演進。
- 難度: 初級
- 學習階段: 核心
- 關聯概念: C-Q1, C-Q3, C-Q7
B-Q22: 為何規模越大越需要在應用層處理一致性?
- A簡: 跨服務/多資料源場景增多,單一 DB 難覆蓋,應用層需負起整體原子性。
- A詳: 隨著微服務與多存儲並存,交易跨出單庫邊界,DB 交易無法直接涵蓋。改以應用層鎖、資源鍵、交易歷史等方式維持整體一致與可追溯;必要時分割資料或混合使用多種儲存。文章示例展示了此思路的可行性與測試方法。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q11, A-Q24, C-Q7
Q&A 類別 C: 實作應用類(10題)
C-Q1: 如何以 C# lock 實作執行緒安全的 ExecTransaction?
- A簡: 以同步物件包住「新增交易紀錄+累加餘額」臨界區,確保互斥與正確。
- A詳: 步驟:1) 定義同步物件 _syncroot;2) 在 ExecTransaction 以 lock(_syncroot) 包住:_history.Add(…) 與 _balance += transferAmount;3) 回傳新餘額。程式碼:lock(_syncroot){ _history.Add(item); return _balance += amount; } 注意:只鎖必要範圍,避免長時間 I/O;使用 decimal 處理金額。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q1, A-Q15, D-Q2
C-Q2: 如何撰寫 N 執行緒 × M 次的並發測試?
- A簡: 建立多執行緒同時執行加值,Join 後比較預期與實際餘額,輸出 PASS/FAIL。
- A詳: 步驟:1) 取得初始餘額;2) 產生 N 個 Thread,每個迭代 M 次呼叫 ExecTransaction(1);3) 啟動所有執行緒並 Join;4) 計算 Expected=origin + N×M;5) GetBalance 比對;6) 以 Stopwatch 記錄吞吐。注意:固定 N、M,避免非必要隨機性;測試前清理狀態。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q9, A-Q14, C-Q10
C-Q3: 如何用 Dapper+T-SQL 交易實作帳務更新?
- A簡: 在單一命令中以 begin/commit 包住 insert+update+select 完成 ACID。
- A詳: 步驟:1) 以 SqlConnection 建立連線;2) 使用 ExecuteScalar 執行多語句:begin tran; insert 交易;update 餘額;select 餘額;commit;3) 回傳查得餘額。程式碼片段見文;注意:確保在同一連線與同一資料庫;必要時使用 TransactionScope;避免長交易。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q3, B-Q18, D-Q3
C-Q4: 如何以 TransactionScope 改寫 SQL 交易?
- A簡: 建立 TransactionScope 包住多次 DB 操作,完成即 Complete() 提交。
- A詳: 步驟:1) using(var scope=new TransactionScope()){ 2) 執行 insert 交易;3) 執行 update 餘額;4) 查詢新餘額;5) scope.Complete(); };注意:若跨資源會升級為更重的交易協調;確保連線在同一範圍內;避免在交易內做外部 I/O。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q17, B-Q3, D-Q3
C-Q5: 如何建立 SQL 表格 schema(accounts、transactions)?
- A簡: 建立 accounts(UserId PK,Balance money) 與 transactions(id PK,userid,time,amount)。
- A詳: 步驟:1) 建立 accounts:UserId nvarchar(50) PK、Balance money not null;2) 建立 transactions:id uniqueidentifier PK、userid nvarchar(50)、time datetime default getdate()、amount money;3) 必要索引與外鍵視情況補充。注意:以 money/decimal 儲存金額;時間以 DB 產生。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q3, A-Q15, D-Q7
C-Q6: 如何用 Docker 快速建立 Redis 與 Mongo 環境?
- A簡: 以 docker run 啟動映像,映射埠口;Redis:6379、Mongo:27017,供鎖與資料存取。
- A詳: 步驟:1) 啟 Redis:docker run -d -p 6379:6379 alexellisio/redis-windows:3.2;2) 啟 Mongo:docker run -d –name mongo -p 27017:27017 mongo:3.4-windowsservercore;注意:確認網路可達;持久化需掛載卷;版本與平台(Windows/Linux)對齊。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q11, C-Q7, D-Q5
C-Q7: 如何用 RedLock.NET 在 Mongo 上實作分散式鎖交易?
- A簡: 以 RedLockFactory 建鎖,鎖內執行查/建帳戶、加餘額、寫歷史,結束釋放。
- A詳: 步驟:1) 建立 RedLockFactory 指向 Redis;2) CreateLock(resource, expiry, wait, retry);3) if(IsAcquired){ 查帳戶,不在則新建;更新 Balance;插入交易歷史;return 新餘額 } else throw;程式碼:using(var l=fac.CreateLock(…)){…}。注意:鎖鍵設計、租約時間、錯誤處理與例外重試。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q5, B-Q6, D-Q4
C-Q8: 如何設計鎖資源鍵與重試/到期參數?
- A簡: 以帳戶為鍵粒度;expiry 覆蓋 P95 耗時;wait/retry 平衡延遲與成功率。
- A詳: 步驟:1) 鎖鍵=”account-transaction::{UserId}”;2) 壓測觀察交易耗時分佈,將 expiry 設為高百分位耗時;3) wait 設最大等待時間,retry 以固定/指數退避;4) 監控失敗率調優。注意:避免過長租約造成等待堆積;避免過短導致過期重入。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: B-Q6, B-Q14, D-Q4
C-Q9: 如何撰寫批次腳本同時啟動多個進程壓測?
- A簡: 以指令迴圈啟動多個程式實例,全部完成後再對帳驗證最終餘額。
- A詳: 步驟(Windows 範例):1) for /l %%i in (1,1,10) do start 程式.exe;2) wait 或以工具等待全部結束;3) 用 Mongo/SQL 查最終餘額;4) 比對期望。注意:控制每個進程執行緒數與迭代次數;避免爭用 IO 與網路資源影響測試。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q10, C-Q2, C-Q10
C-Q10: 如何產出與解讀測試輸出(Expected/Actual/Performance)?
- A簡: 輸出預期與實際餘額與每秒交易數,以 PASS/FAIL 判斷正確與比較效率。
- A詳: 步驟:1) 計算 expectedBalance=origin + N×M;2) 讀 actualBalance;3) 比較是否相等→PASS/FAIL;4) Performance=N×M×1000/ElapsedMs;5) 紀錄測試日期、參數、實作版本。注意:確保讀餘額時所有執行緒/進程已完成;效能比較需同環境。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q9, B-Q10, A-Q14
Q&A 類別 D: 問題解決類(10題)
D-Q1: 餘額不等於預期(單機)怎麼辦?
- A簡: 多半是未鎖臨界區或鎖範圍不正確,補上互斥並覆測,確保完整包住更新。
- A詳: 症狀:Actual < Expected。原因:未鎖或鎖外仍有共享更新。解法:以 lock 包住「寫歷史+加餘額」,避免跨鎖存取共享狀態。步驟:檢視程式區塊、補鎖、回歸測試。預防:程式碼評審、單元測試常態化。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: C-Q1, B-Q2, A-Q4
D-Q2: 發生競態條件如何處理?
- A簡: 用鎖保護「讀改寫」序列或改用原子操作,避免交錯覆蓋與丟失更新。
- A詳: 症狀:偶爾少扣/多扣、測試結果不穩。原因:並發讀寫交錯。解法:將讀→計算→寫入包成臨界區;必要時使用 Interlocked/原子操作。步驟:重現問題→鎖定臨界區→覆測。預防:設計時辨識共享狀態、添加並發測試。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q5, B-Q1, C-Q2
D-Q3: SQL 交易偶發死鎖怎麼辦?
- A簡: 檢查語句順序與索引,重試失敗交易,必要時調整隔離層級或拆交易。
- A詳: 症狀:交易被回滾,訊息顯示 deadlock。原因:資源相互等待。解法:統一存取順序、加適當索引降低鎖範圍;加入重試;視情況調整隔離層級。步驟:分析執行計畫與 deadlock graph→優化 SQL→加重試。預防:縮短交易時間,避免不必要掃描。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q3, B-Q4, C-Q3
D-Q4: 分散式鎖無法取得(頻繁超時)怎麼辦?
- A簡: 熱點競爭或參數不當,調整 expiry/wait/retry,分散熱點或拆細鎖粒度。
- A詳: 症狀:IsAcquired 常為 false。原因:熱鍵競爭、租約太長或等待太短。解法:縮短臨界區、調整 expiry 覆蓋 P95;增加 wait、合理 retry 退避;拆細資源鍵降低爭用。步驟:觀測成功率→微調參數→壓測驗證。預防:避免臨界區內 I/O。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: B-Q6, B-Q14, C-Q8
D-Q5: Redis 斷線導致鎖失敗怎麼辦?
- A簡: 對鎖取得失敗做明確處理與重試;確保 Redis 可用性與監控告警。
- A詳: 症狀:CreateLock 逾時或例外。原因:網路/服務故障。解法:實作重試與回退策略,鎖失敗時回報或延後;確保 Redis 穩定部署與監控。步驟:補上例外處理→加重試→檢查基礎設施。預防:健康檢查、告警、容量規劃。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: C-Q6, C-Q7, B-Q11
D-Q6: 程式崩潰導致鎖未釋放怎麼辦?
- A簡: 使用租約到期自動釋放;控制臨界區時長,必要時縮短 expiry。
- A詳: 症狀:其他節點長時間卡鎖。原因:持鎖方崩潰未釋放。解法:設定合理 expiry,確保自動回收;縮短臨界區避免超時;監控異常持鎖時間。步驟:調整 expiry→監控→壓測故障場景。預防:臨界區內避免外部阻塞操作。
- 難度: 中級
- 學習階段: 核心
- 關聯概念: A-Q22, B-Q6, C-Q8
D-Q7: SQL 交易效能不佳如何優化?
- A簡: 優化 SQL 與索引、縮短交易、分割資料與負載,必要時移部分一致性到應用層。
- A詳: 症狀:TPS 低、延遲高。原因:鎖競爭、全表掃描、IO 飽和。解法:加索引、減少掃描、縮短交易範圍;分表分庫或 sharding;或改用 NoSQL+分散式鎖。步驟:定位瓶頸→調 SQL/索引→壓測比較。預防:容量規劃與監控。
- 難度: 中級
- 學習階段: 進階
- 關聯概念: A-Q25, B-Q17, A-Q24
D-Q8: 測試結果偶爾 FAIL 如何診斷?
- A簡: 檢查 Join 是否完整、環境是否干擾、參數是否固定,增強可重現性。
- A詳: 症狀:偶發不一致。原因:未等待所有執行緒/進程完成、外部狀態干擾。解法:確認 Thread.Join 與進程等待、固定 N/M、清理初始狀態。步驟:加日誌→最小化環境變因→逐步排除。預防:建立可重現的測試腳本與資料重置步驟。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: B-Q9, C-Q2, C-Q9
D-Q9: 金額出現精度問題怎麼解?
- A簡: 改用 decimal 儲存與計算,避免 double 的二進位小數誤差累積。
- A詳: 症狀:小數誤差導致對帳不平。原因:浮點表示無法精準表示十進制小數。解法:全程使用 decimal;避免不同型別混用。步驟:檢查資料型別→修改程式與 DB 欄位→回歸測試。預防:規範金額型別與轉換。
- 難度: 初級
- 學習階段: 基礎
- 關聯概念: A-Q15, C-Q3, C-Q5
D-Q10: 發現交易歷史與餘額不一致怎麼辦?
- A簡: 檢查臨界區是否完整包住兩步,補上鎖或單一交易,確保原子性。
- A詳: 症狀:有歷史無餘額或相反。原因:步驟分離或未受鎖保護。解法:單機用 lock 包整段;DB 用交易;分散式用鎖後串行執行兩步。步驟:重構 ExecTransaction→覆測。預防:單元測試覆蓋兩者一致性斷言。
- 難度: 初級
- 學習階段: 核心
- 關聯概念: B-Q7, C-Q1, C-Q7
學習路徑索引
- 初學者:建議先學習哪 15 題
- A-Q1: 什麼是「線上交易的正確性」?
- A-Q2: 什麼是「金錢守恆定律」?
- A-Q3: 為何交易「不能只做一半」?
- A-Q4: 什麼是「臨界區」(Critical Section)?
- A-Q5: 什麼是「競態條件」(Race Condition)?
- A-Q6: 什麼是「Lock」?為何需要?
- A-Q7: 什麼是 ACID?
- B-Q1: 單機 Lock 解法如何運作?
- B-Q2: 未加鎖為何會少錢?背後機制是什麼?
- B-Q9: 測試程式如何驗證正確性?
- C-Q1: 如何以 C# lock 實作執行緒安全的 ExecTransaction?
- C-Q2: 如何撰寫 N 執行緒 × M 次的並發測試?
- C-Q10: 如何產出與解讀測試輸出?
- D-Q1: 餘額不等於預期(單機)怎麼辦?
- D-Q2: 發生競態條件如何處理?
- 中級者:建議學習哪 20 題
- A-Q8: 什麼是資料庫的併發控制?
- A-Q9: 應用層 Lock 與 SQL 交易的差異?
- A-Q10: RDBMS 與 NoSQL 在交易處理上的差異?
- A-Q11: 為何微服務下仍須嚴格交易控制?
- A-Q12: 什麼是分散式鎖(Distributed Lock)?
- A-Q13: 什麼是 RedLock?文中如何使用?
- A-Q14: 為什麼以「N×M×+1 元」驗證正確性?
- A-Q16: 本地鎖與分散式鎖有何差異?
- A-Q17: TransactionScope 與 T-SQL 的差異?
- A-Q18: 為何 SQL 交易通常限制在單一資料庫?
- B-Q3: SQL 交易如何保證 ACID?
- B-Q5: 分散式鎖如何運作?
- B-Q6: RedLock.NET 參數如何影響行為?
- B-Q10: 多程序測試如何驗證分散式鎖有效?
- B-Q11: Docker 在實驗環境中的角色?
- C-Q3: 如何用 Dapper+T-SQL 交易實作帳務更新?
- C-Q6: 如何用 Docker 建立 Redis/Mongo?
- C-Q7: 如何用 RedLock.NET 實作分散式鎖交易?
- D-Q4: 分散式鎖無法取得(超時)怎麼辦?
- D-Q5: Redis 斷線導致鎖失敗怎麼辦?
- 高級者:建議關注哪 15 題
- A-Q19: 何謂 CAP 與 ACID/BASE 的關聯?
- A-Q21: 為何在 NoSQL 上仍可保證交易正確?
- A-Q22: 鎖的「重試」與「到期」角色?
- A-Q23: 不同規模對應策略為何?
- A-Q24: 資料分割/分片在擴展中的角色?
- A-Q25: DBMS 作為單點瓶頸的風險是什麼?
- B-Q4: 隔離層級的作用是什麼?
- B-Q13: CAS 是什麼?與鎖的關係?
- B-Q14: 重試/退避策略對系統影響?
- B-Q16: 鎖參數選擇如何影響正確與效能?
- B-Q17: 為何 DB 交易吞吐較低?
- B-Q22: 為何規模越大越需應用層處理一致性?
- C-Q8: 如何設計鎖鍵與參數?
- D-Q7: SQL 交易效能不佳如何優化?
- D-Q6: 程式崩潰導致鎖未釋放怎麼辦?