閒談軟體設計:樂觀鎖

更新 發佈閱讀 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
Spirit的沙龍
58會員
117內容數
這是從 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
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
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