架構與思維:漫談高併發業務的CAS及ABA

Hello-Brand發表於2024-10-10

1 高併發場景下的難題

1.1 典型支付場景

這是最經典的場景。支付過程,要先查詢買家的賬戶餘額,然後計算商品價格,最後對買家進行進行扣款,像這類的分散式操作,
如果是併發量低的情況下完全沒有問題的,但如果是併發扣款,那可能就有一致性問題。在高併發的分散式業務場景中,類似這種 “查詢+修改” 的操作很可能導致資料的不一致性。
image

1.2 線上下單場景

同理,買家在電商平臺下單,往往會涉及到兩個動作,一個是扣庫存,第二個是更新訂單狀態,庫存和訂單一般屬於不同的資料庫,需要使用分散式事務保證資料一致性。
image

1.3 跨行轉賬場景

跨行轉賬問題也是一個典型的分散式事務,使用者A同學向B同學的賬戶轉賬500,要先進行A同學的賬戶-500,然後B同學的賬戶+500,既然是 不同的銀行,涉及不同的業務平臺,為了保證這兩個操作步驟的一致,資料一致性方案必然要被引入
image

2 CAS方案

分散式CAS(Compare-and-Swap)模式就是一種無鎖化思想的應用,它透過無鎖演算法實現執行緒間對共享資源的無衝突訪問,既保證效能高效,有保證資料的強一致性,避免了上面集中問題的產生。
CAS模式包含三個基本運算元:記憶體地址V、舊的預期值A和要修改的新值B。在更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。

我們以 1.1節典型支付場景 作為例子分析(參考下圖):

  • 初始餘額為 800
  • 業務1和業務2同時查詢餘額為800
  • 業務1執行購買操作,扣減去100,結果是700,這是新的餘額。理論上只有在原餘額為800時,扣減的Action才能執行成功。
  • 業務2執行生活繳費操作(比如自動交電費),原餘額800,扣減去200,結果是600,這是新的餘額。理論上只有在原餘額為800時,扣減的Action才能執行成功。可實際上,這個時候資料庫中的金額已經變為600了,所以業務2的併發扣減不應該成功。

根據上面的CAS原理,在Swap更新餘額的時候,加上Compare條件,跟初始讀取的餘額比較,只有初始餘額不變時,才允許Swap成功,這是一種常見的降低讀寫鎖衝突,保證資料一致性的方法。
image

3 引出ABA問題

在CAS(Compare-and-Swap)操作中,ABA問題是一個常見的挑戰。這邊假設三個運算元——記憶體位置(V)、預期原值(A)和新值(B)。
ABA問題是指當某個執行緒讀取一個共享變數V的值為A,之後準備將其更新為B時,另一個執行緒可能已經將其從A改為了B,然後又改回了A。
此時,當前執行緒仍認為V的值是原始的A,因此CAS操作會將V的值更新為B,但實際上V的值已經被其他執行緒改變過。

image

它有如下危害:

1. 資料一致性受損,並導致業務邏輯錯誤
在複雜的業務邏輯中,共享變數的值往往代表了某種業務狀態或條件。ABA問題可能導致這些狀態或條件被意外地改變,從而引發業務邏輯錯誤,如庫存超賣、資金重複發放等
★ 以下的圖詳細描述了ABA是怎麼導致庫存邏輯出錯的:
image

2. 難以除錯與定位
ABA問題通常發生在多執行緒環境下,且其觸發條件較為隱蔽。因此,當系統出現由ABA問題導致的異常時,往往難以快速定位問題原因,增加了除錯的複雜性和時間成本。

4 不同維度的處理方式

ABA出現的原因,是CAS的過程中,只關注Value值的校驗。但是忽略了這個值還是不是之前的那個值,可以參考上面的庫存圖例。所以某些情況下,Value雖然相同,卻已經不是原來的資料了。

解決方案:CAS不能只比對 Value,還必須確保的是原來的資料,才能修改成功。
一般的做法是,給 Value 設定一個Version(版本號),用來比對,一個資料一個版本,每次資料變化的時候版本跟隨變化,這樣的話就不會隨隨便便修改成功。

4.1 應用程式層

Java中的java.util.concurrent.atomic包提供瞭解決ABA問題的工具類。
在Go語言中,通常使用sync/atomic包提供的原子操作來處理併發問題,並引入版本號或時間戳的概念。
示例程式碼如下:

type ValueWithVersion struct {  
	Value     int32  
	Version   int32  
}  
  
var sharedValue atomic.Value // 使用atomic.Value來儲存ValueWithVersion的指標  
  
func updateValue(newValue, newVersion int32) bool {  
	current := sharedValue.Load().(*ValueWithVersion)  
	if current.Value == newValue && current.Version == newVersion {  
		// CAS操作:只有當前值和版本號都匹配時,才更新值  
		newValueWithVersion := &ValueWithVersion{Value: newValue, Version: newVersion + 1}  
		sharedValue.Store(newValueWithVersion)  
		return true  
	}  
	return false  
}  

4.2 資料層

  1. CAS策略
update stock set num_val=$num_new_val where sid=$sid and num_val=$num_old_val
  1. CAS策略+Version,避免ABA問題
# 這邊注意,有了version,就沒必要再比較old_val了
update stock set num=$num_new_val, version=$version_new where sid=$sid and version=$version_old

5 總結

  1. 高併發下的難題:支付、下單、跨行轉賬
  2. CAS方案以及引發的ABA問題
  3. 不同維度的處理方式:應用層、資料層

啦啦啦啦啦啦,不寫了去跑步啦

相關文章