資料一致性(二)
我們流連於事物的表象,滿足淺嘗輒止的片刻歡愉,卻幾乎從不久留。我們在人生的道路上爭先恐後,卻吝於用片刻思考目標和方向。
概述
至今沒有接觸過MySQL
多主的情況,即存在多個MySQL
例項同時負責讀寫請求(拋棄只讀庫)。思考後認為:沒有這麼實現的技術難點在於:資料的一致性得不到保證。此外,還會涉及:
MySQL
那麼支援分散式的其他資料庫又是怎麼搞定這個問題的呢?比如Cassandra
,多個節點之間可以同時處理讀寫請求,那麼它是如何處理節點間資料同步以保證一致性的呢?
MySQL資料的一致性
We think this is an unacceptable burden to place ondevelopers and that consistency problems should be solved at the database level
細細想想,MySQL
自身實現的資料一致性也是相當複雜的。以Innodb
舉例,如果通過普通索引執行查詢,首先獲取到的僅僅是主鍵索引,後面還需要通過主鍵索引來獲取完整的記錄。查詢如此,更新亦如此。
Master-Slave
模式
通常情況下,MySQL
部署都是一主多從。Master
作為更新DB
的入口,而Slave
的資料通過binlog
來進行同步。所以大膽想一想
,有沒有可能出現一種情況(假設id=1
記錄原始的name
值為neojos
):
## 第一次同步資料 update s-1 set name="neojos-1" where id = 1;## 失敗 update s-2 set name="neojos-1" where id = 1;## 成功 update s-3 set name="neojos-1" where id = 1;## 成功 ## 第二次同步資料 update s-1 set name="neojos-2" where id = 1;## 成功 update s-2 set name="neojos-2" where id = 1;## 失敗 update s-3 set name="neojos-2" where id = 1;## 成功
最後,資料庫從某一個時間點開始,Master
和Salve
的資料會變得不一致了當然不可能,MySQL
在資料同步上做了非常硬的約束。包括Slave_IO_Running
、Slave_SQL_Running
以及
Seconds_Behind_Master
等。
併發下的資料一致性
MySQL
併發下的資料一致性是通過鎖來保證的。併發的請求,誰先拿到
X
鎖
,誰就有修改的許可權。鎖類似扮演了一個操作版本號的作用。
X
|
IX
|
S
|
IS
|
|
---|---|---|---|---|
X
|
Conflict | Conflict | Conflict | Conflict |
IX
|
Conflict | Compatible | Conflict | Compatible |
S
|
Conflict | Conflict | Compatible | Compatible |
IS
|
Conflict | Compatible | Compatible | Compatible |
理解衝突
以資料讀取和寫入為切入點,引申出兩個工作中可能可能遇到的衝突問題,並通過加鎖以及設定版本號來避免衝突的發生。
Go的併發問題
下面是一個簡單的Go Test
程式碼問題:求1-100的累加和。我們通過Goroutine
和最普通的兩個方式分別計算。同時,在程式碼的末尾對兩種方式的計算結果進行了比較並列印輸出。
// 輸出結果每次都是變化的。其中一次:5499 != 5050 func TestSum1To100(t *testing.T) { result1 := 0 result2 := 0 // 併發的進行計算 var wg sync.WaitGroup for i := 1; i <= 100; i++ { wg.Add(1) go func(m int) { defer wg.Done() result1 += m }(i) } // 正常的For迴圈 for i := 1; i <= 100; i++ { result2 += i } wg.Wait() if result1 != result2 { t.Fatalf(" %d != %d", result1, result2) } }
併發情況下,每個goroutine
在讀取result1
到result1=result1+1
的過程中,無法保證result1
不被別的goroutine
所修改。
從MySQL
解決問題的思路來考慮:加鎖。我們要對讀取result1
到result1=result1+1
的過程進行加鎖,保證這個過程是同步的。
一對多情況
在國內第三方支付(微信/支付寶)場景中,使用者是否支付了某個商品,是通過服務端接受第三方非同步回撥通知的方式,來作為判斷依據的。而回調通知存在相應的重試策略,而且都要求冪等處理。
假設下面一個場景,我們建立了以user_id
為唯一索引的表(user_pay
)用於統計該使用者支付成功的次數,以及使用者支付明細表(user_pay_detail
),兩者是一對多的對應關係。服務端每次收到第三方的支付回撥,都在user_pay_detail
追加一條新記錄,同時相應的調整user_pay
的資訊。
如果在回撥過程中,存在這樣一個場景:在03-02
號收到了支付回撥通知,對資料進行了調整。而在03-15
號的時候卻又收到了02-01
的回撥通知(該通知已經在02-01
處理過了)。如何保證user_pay
中的資料不會被多加一次?
當然,解決辦法非常簡單。其中一個解決辦法便是:在user_pay
中記錄上一次回撥通知的時間戳,以此作為這行記錄的版本號,後續也只有大於該版本號的通知才會被處理。
CAP
瞭解一下分散式的環境下的CAP
定理,這裡主要強調一下:Consistency
。在分散式系統中,存在多節點同時對外提供讀寫服務,資料儲存多份副本的情況。那麼,這些節點在同步資料的過程中,可能會因為網路或者機器的原因導致資料同步失敗,從而造成各個節點資料不一致的情況發生。
Last-write-wins
Last-write-wins
表示在對一條記錄應用多個修改的時候,最後的改動會覆蓋掉之前的操作,返回給客戶端的記錄都以最後一次的改動為準。
這也是分散式系統解決衝突的一個策略。基於timestamp
的版本控制系統,比如HBase
。每次操作都會給記錄附加一個timestamp
的版本號。這樣一來,當某些資料發生衝突時,我們就可以簡單的認為最新的記錄是準確的。
但實際上,基於Last-write-wins
的策略並不一定是正確的。比如多個節點對同一條記錄進行修改。首先,節點服務上的時間鐘不是嚴格相等的;其次,客戶端發出的請求時間,跟到達節點服務的時間也是沒有任何聯絡的。
vector clock
先說一下需要用到向量時鐘的場景。我們在寫資料時候,經常希望資料不要儲存在單點。如db1,db2都可以同時提供寫服務,並且都存有全量資料。而client不管是寫哪一個db都不用擔心資料寫亂問題。但是現實場景中往往會碰到並行同時修改。導致db1和db2資料不一致。於是乎就有人想出一些解決策略。向量時鐘算是其中一種。簡單易懂。但是並沒有徹底解決衝突問題,現實分散式儲存補充了很多額外技巧。
文章vector clock 向量時鐘演算法 解釋的實在是太完美了,這裡就不冗餘解釋了。下圖是一個分散式服務的示例,各個節點都可以提供讀寫服務。
Cassandra
的思路
KV
型別的分散式資料庫在儲存物件時,儲存的是物件序列化的結果。舉個例子:
-
有一個
jbellis
的物件,初始值為{'email': '[email protected]', 'phone': '555-5555'}
,我們認為這個初始值為V0
-
之後修改了
jbellis
的郵件地址,這時候值記作V1
,{'email': '[email protected]', 'phone': '555-5555'}
。但因為網路或其他問題,在同步資料到其他節點的時候失敗了,導致該修改僅僅被成功寫到了其中一個節點上 -
接著,我們更新
jbellis
中的電話資訊。但我們讀取到的jbellis
是V0
,所以,修改後的V3
為{'email': '[email protected]', 'phone': '444-4444'}
從Last-write-wins
的角度考慮,我們採納了V2
的值,而丟棄了V1
。簡單直接,但不一定正確;
從vector clock
的角度來看,當同步V2
到其他節點時,就會發生資料衝突,因為當前節點的版本為[V0, V2]
,而其他節點的版本是[V0, V1]
,這時候就需要依靠具體的衝突解決策略。
而Cassandra
在儲存資料結構上做了處理,將物件中email
和phone
單獨儲存,並給每個column
都指定一個獨立的timestamp
作為版本號。這樣,當衝突發生時,就可以簡單運用Last-write-wins
策略了。
A column is the basic data structure of Cassandra with three values, namely key or column name, value, and a timestamp. Given below is the structure of a column.
總結
實事求是,具體問題具體分析。請記住,對你而言,上面這些方法可能都不合適。