閒談軟體設計:樂觀鎖

更新 發佈閱讀 6 分鐘
圖片來源:ChatGPT 生成

圖片來源:ChatGPT 生成

長期寫後端程式,應該都會有類似的經驗,系統大多數時間行為都是對的,突然回報了某個 bug,還非常難重現,偶而才出現一次,程式碼追了半天,看不出任何問題,最後從 log 發現,是兩個幾乎同時的 request 以不預期的順序執行,導致最終狀態是錯的,也就是 race condition 沒處理好。

Race Condition

一般來說,所有操作資料庫的程式都可能會遇上 race condition,只是最終結果不一定是 bug,或是說不會造成嚴重的後果。例如:某個資料表代表某日的公告,當使用者 A 和使用者 B 同時修改同一日的公告,最理想的狀況是同時保留 A 與 B 的修改,最差的結果是只留下 A 或 B 的修改,那最差的結果算是 bug 嗎?很難說。但如果是某個帳戶的餘額,若有兩個提領的操作,沒處理好 race condition 的話,最嚴重是銀行多付錢,餘額還是錯的,這時就是嚴重的 bug 了。

過去在處理訂單等對金錢比較敏感的系統時,大多會使用鎖,像是用 Redis 的鎖或是用資料庫高強度的交易隔離等級來確保執行順序,這類的鎖都是悲觀鎖,確保同時只有一個請求能執行,但如果鎖的範圍不洽當,雖然可以確保系統的正確性,卻犧牲了系統的吞吐量,對於水平擴展來說是個限制。

樂觀鎖

這次新系統的開發,都是採取樂觀鎖,這是取決在一個前提:新的系統即便是高併發,對於「同一筆」資料修改的「頻率」也不會很高。這種情況特別適合使用樂觀鎖。

樂觀鎖的實作很簡單,不過仍需要對 domain 中的 Entity 加入額外的欄位:version,算是還可以接受的小汙染。

如果不想加 version,最接近,能複用的欄位應該是 updatedTime 了,但資料庫儲存時間的資料格式可能是浮點數,在比較時結果不一定符合預期。

接著,在 Repository 的實作中加入對 version 的比對檢查,只有當資料庫該筆資料的版號依然是「預期中的版號」時才更新,並將 version 加 1,不然就視作錯誤。

在修改 entity 時,不需要去更動 version

這時回到服務層,當兩個編輯同一個公告的請求 A 與 B 近來,若請求的公告存在,在第 6 行都會得到同個版本的公告,假設版本是 6,此時,如果是 A 先完成儲存的動作,當 B 執行儲存時,會因為版本已經跳到 7 了,儲存失敗。此時,有多種選擇,一個是單純重試,但結果可能是把 A 的編輯覆蓋掉,或是前端重新取得公告,由 B 決定怎麼處理,然後再送出請求。

進階應用

上述的例子,只是一個簡單的 Entity,但樂觀鎖也可以用在確保複雜的結構完整性,例如:有個 Entity A,在商業邏輯中最多可以新增兩個 Entity B,這樣的商業邏輯可以很簡單地寫在 Entity A 中 (第 8 至第 13 行),也可以用簡單的單元測試確保邏輯正確。

假設同時有兩個請求 X 與 Y,試著對一個已經有一個 child 的 entity A 加入第二個 child 時,當 X 成功完成請求,版本會加 1,此時另一個請求 Y 會因為版本不同而失敗。這可以讓商業邏輯的組成,更容易以物件導向的方式去設計,而不是程序導向的方式 (先檢查已經加到 a 的 b 筆數有幾筆,再進行把 b 加到 a 的動作)。

注意事項

樂觀鎖用起來確實很簡單,但也是有幾點要注意,不然仍可能造成問題:

  • 所有的「更新」動作都要檢查 version,因此用 Repository 封裝資料庫的操作,然後只透過 Repository 操作資料庫會是最方便的。
  • 建議仍用 transaction 把資料庫操作包起來,雖然不需要 SELECT FOR UPDATE 或是設定隔離層級,但如果要更新複雜的資料結構,當 version 不對時拋出例外後,roll back 能確保不會留下髒資料。

小結

新系統在使用樂觀鎖後,個人是相當滿意,確保正確性是一回事,最讓我滿意的是,整個系統中,除了 Entity 加了 version 這個特殊欄位,沒有任何一個地方要加入鎖的概念,不像過去要在很多地方要寫「取得某個鎖」然後進行動作,讓程式的邏輯好讀很多。

不過,樂觀鎖並不是沒有缺點,在高併發的情況下,假設 n 個請求修改同一筆資料,只會有一個成功,其餘 n - 1 個都會失敗,即便 n 個請求修改同一筆資料的不同屬性。因此,在高併發的情況下,悲觀鎖可能是比較好的解法,要視需求做出不同的選擇。

留言
avatar-img
留言分享你的想法!
avatar-img
Spirit的沙龍
58會員
111內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
Spirit的沙龍的其他內容
2025/12/14
現今的系統不論是 B to B、B to C 或是 B to B to C,通知都是不可少,不管是簡訊發送 OTP,還是發送臨時密碼的 email,或各式各樣的 push 通知,通知已不可少的環節,這也是為什麼在一開始系統架構設計時,早早把 ncc 規劃成一個獨立模組 (子系統)。
Thumbnail
2025/12/14
現今的系統不論是 B to B、B to C 或是 B to B to C,通知都是不可少,不管是簡訊發送 OTP,還是發送臨時密碼的 email,或各式各樣的 push 通知,通知已不可少的環節,這也是為什麼在一開始系統架構設計時,早早把 ncc 規劃成一個獨立模組 (子系統)。
Thumbnail
2025/12/13
本文深入探討了 UUID 的演進,介紹了 UUID v6 和 v7 相較於舊版本在時間排序上的顯著提升,以及 ULID 作為另一種優化 ID 設計的替代方案。技術是不斷進化的,定期檢視是必要的。
Thumbnail
2025/12/13
本文深入探討了 UUID 的演進,介紹了 UUID v6 和 v7 相較於舊版本在時間排序上的顯著提升,以及 ULID 作為另一種優化 ID 設計的替代方案。技術是不斷進化的,定期檢視是必要的。
Thumbnail
2025/11/22
分享自身團隊在 Cloudflare 當機時,如何透過事先規劃的備援機制,在極短時間內將服務切換至 GCP Cloud DNS 並恢復正常運作的經驗。文章深入探討備援設計的複雜性,涵蓋成本、同步、複雜度及演練等面向,並總結事後檢討,強調建置外部監控系統和自動化 SSL 憑證更新的重要性。
Thumbnail
2025/11/22
分享自身團隊在 Cloudflare 當機時,如何透過事先規劃的備援機制,在極短時間內將服務切換至 GCP Cloud DNS 並恢復正常運作的經驗。文章深入探討備援設計的複雜性,涵蓋成本、同步、複雜度及演練等面向,並總結事後檢討,強調建置外部監控系統和自動化 SSL 憑證更新的重要性。
Thumbnail
看更多
你可能也想看
Thumbnail
不是每個人都適合自己操盤,懂得利用「專業」,才是績效拉開差距的開始
Thumbnail
不是每個人都適合自己操盤,懂得利用「專業」,才是績效拉開差距的開始
Thumbnail
PyTorch 是一個開源的 Python 機器學習庫,基於 Torch 庫,底層由 C++ 實現,應用於人工智慧領域,如電腦視覺和自然語言處理等。 PyTorch 2.4 引入了多項新功能和改進,包括支援 Python 3.12、AOTInductor 凍結功能、新的高階 Python 自訂運算
Thumbnail
PyTorch 是一個開源的 Python 機器學習庫,基於 Torch 庫,底層由 C++ 實現,應用於人工智慧領域,如電腦視覺和自然語言處理等。 PyTorch 2.4 引入了多項新功能和改進,包括支援 Python 3.12、AOTInductor 凍結功能、新的高階 Python 自訂運算
Thumbnail
NumPy 是 Python 語言的一個擴充程式庫,支援高階大規模的多維陣列與矩陣運算的數學函式函式庫。 NumPy 2.0.0 是自 2006 年以來的第一個主要發行版本,此重要版本標誌著 NumPy 發展歷程中的一項重要里程碑,為使用者提供了豐富的增強功能和改進,並為未來的功能開發奠定了基礎。
Thumbnail
NumPy 是 Python 語言的一個擴充程式庫,支援高階大規模的多維陣列與矩陣運算的數學函式函式庫。 NumPy 2.0.0 是自 2006 年以來的第一個主要發行版本,此重要版本標誌著 NumPy 發展歷程中的一項重要里程碑,為使用者提供了豐富的增強功能和改進,並為未來的功能開發奠定了基礎。
Thumbnail
Selenium 是一個範圍廣泛的工具和函式庫的總稱專案,用於啟用和支援網頁瀏覽器的自動化。Selenium WebDriver 提供了 C#、JavaScript、Java、Python、Ruby 等多種語言的 API,可以用於編寫自動化測試軟體。 在定位元素時,WebDriver 提供對這 8
Thumbnail
Selenium 是一個範圍廣泛的工具和函式庫的總稱專案,用於啟用和支援網頁瀏覽器的自動化。Selenium WebDriver 提供了 C#、JavaScript、Java、Python、Ruby 等多種語言的 API,可以用於編寫自動化測試軟體。 在定位元素時,WebDriver 提供對這 8
Thumbnail
JavaScript (簡稱 JS) 是具有一級函數的輕量級、直譯式或即時編譯的程式語言。它因為用作網頁的腳本語言而大為知名,但也用於許多非瀏覽器的環境,像是 Node.js 等。由於 JavaScript 語法上的一些缺點,軟體工程師們又設計出了 CoffeeScript、TypeScript 和
Thumbnail
JavaScript (簡稱 JS) 是具有一級函數的輕量級、直譯式或即時編譯的程式語言。它因為用作網頁的腳本語言而大為知名,但也用於許多非瀏覽器的環境,像是 Node.js 等。由於 JavaScript 語法上的一些缺點,軟體工程師們又設計出了 CoffeeScript、TypeScript 和
Thumbnail
樣板模式的定義極為簡單,卻是大型系統程式、WEB/APP應用框架的設計核心,完美展現設計模式的價值: 簡單、高效、強大。
Thumbnail
樣板模式的定義極為簡單,卻是大型系統程式、WEB/APP應用框架的設計核心,完美展現設計模式的價值: 簡單、高效、強大。
Thumbnail
終於完成物件導向的設計,包括用於指導撰寫程式的「類別模型」和「動態模型」。以物件導向的方式進行設計,只是進攻的前奏,撰寫程式才是最終的目標。雖然物件導向的理論、方法、技巧經過多年的發展後,業界已經形成基本統一的認知,但並未出現一種統一的「物件導向程式語言」。
Thumbnail
終於完成物件導向的設計,包括用於指導撰寫程式的「類別模型」和「動態模型」。以物件導向的方式進行設計,只是進攻的前奏,撰寫程式才是最終的目標。雖然物件導向的理論、方法、技巧經過多年的發展後,業界已經形成基本統一的認知,但並未出現一種統一的「物件導向程式語言」。
Thumbnail
封裝、繼承、多型是物件導向的三大核心特徵,判斷一種程式語言是否為物件導向的程式語言,就看其是否支援這三大核心特徵。 軟體類別是對現實類別的模擬,但不是簡單的等同。除了實作現實類別相對應的功能,還會創造出許多現實中不存在的類別。 這個創造過程正是各種設計方法、設計模式、設計原則大顯身手的地方。
Thumbnail
封裝、繼承、多型是物件導向的三大核心特徵,判斷一種程式語言是否為物件導向的程式語言,就看其是否支援這三大核心特徵。 軟體類別是對現實類別的模擬,但不是簡單的等同。除了實作現實類別相對應的功能,還會創造出許多現實中不存在的類別。 這個創造過程正是各種設計方法、設計模式、設計原則大顯身手的地方。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News