前面介紹了 CAP 定理,得知了在分散式系統中,資料的同步只能 C3 取 2。
CP 確實是比較難以理解的一個選項,因此在此舉個 CP 的同步方式,
因此,有一個簡單且有效的作法 2PC/3PC 可以幫我們達到以上三個目的。在介紹 2PC/3PC 前,
我們先來思考與試錯,一步一步的了解該項目的重點與解決的項目。
讓兩個獨立的 DB 加入新的資料,並且一起成功或一起失敗,不會有其中一台 DB 成功,一台 DB 失敗。
先回歸操作一台 db 的樣子,
當我們要操作 db 時,我們會先寫一個 server,讓該 server 連上 db 下 CRUD 等操作。
當我們要操作 n 座 db 時,一樣的寫一個 server,讓該 server 連上那些 db 下 CRUD 等操作。
如果以這個案例來定義,我們將會稱該 server 為協調者,而 db 就是我們的參與者。
今天有兩台 db,分別為 A 與 B,A 插入一筆紀錄,B 也必須插入一筆一樣的紀錄。
以下 sql 都是在協調者的操作
| time | db A | db B |
|---|---|---|
| 1 | begin | |
| 2 | insert... | |
| 3 | commit | |
| 4 | begin | |
| 5 | insert... | |
| 6 | commit |
非常正常的操作,先完成 db A 的操作,再去完成 db B 的操作。
但假設 time5 發生錯誤,db A 就無法 rollback,因為 connection A 的 tx 已經 commit 了,
必須使用手動刪除,兩 db 之間會有軟狀態(暫時性的不一致),與我們想要達成的強一致性不符合。
| time | db A | db B |
|---|---|---|
| 1 | begin | |
| 2 | begin | |
| 3 | insert... | |
| 4 | insert... | |
| 5 | commit | |
| 6 | commit |
你一句我一句,看似兩者膠著狀態,但其實 time6 發生錯誤,也是如同 try1 一樣無法 rollback。
不管如何,commit 時才發生錯誤都無法達到 rollback。
因為只要其中一個 commit 了,另一個發生錯誤就無法挽回了。
有沒有一個 sql 語句可以模擬 commit,提前知道錯誤,如果有錯誤就 rollback。
如果這個模擬 commit 沒發生錯誤,那麼我們就可以假設,接下來的 commit 基本上不會發生錯誤了。
你可能會想,通常 TX 到 COMMIT 之間,要發生錯誤早就發生了,甚麼錯誤會發生在下 COMMIT 之後呢?
其實最常見的就是違反 UNIQUE constraint,這裡稍微列出一些 COMMIT 之後才會發生的錯誤。
裡面我們撇除物理意外等不可控的因素外,希望至少能製造一個 SQL 語句可以模擬 commit,提前知道錯誤。
希望至少能製造一個 SQL 語句可以模擬 commit,提前知道錯誤。
那麼我們就可以假設,接下來的 commit 基本上不會發生錯誤了。
這個需求,就是 2PC 的重點核心準備階段 prepare statement。
一般的 tx 就是一個 commit 後直接見真章,是否發生錯誤無法事先得知,這就是我們一般俗稱的一階段提交 1PC。
而這裡我們將加上準備階段 prepare statement,是否發生錯誤透過準備階段事先得知,
就可以假設,接下來的 commit 基本上不會發生錯誤;如果發生錯誤,那麼就可以在 commit 之前做 rollback。
所以加上PREPARE這就是我們鼎鼎大名的 2PC(two-phase commit)啦!
而 3PC 也是相同概念,不過分成三個階段canCommit、preCommit、doCommit,
不過由於 3PC 大部分的資料庫不支援,所以就不再此贅述了。
| time | db A | db B |
|---|---|---|
| 1 | begin | |
| 2 | insert... | |
| 3 | prepare ... | |
| 4 | begin | |
| 5 | insert... | |
| 6 | prepare ... | |
| 6 | commit | |
| 7 | commit |
加上了 prepare statement,就可以在 commit 前做個預檢查,確定了不會發生錯誤,在做 commit,大大減少在 commit 發生的錯誤。
不過 2PC 的缺點也是一樣,只能保證 commit 前的錯誤,只是增加一個 statement 來做預檢查。
如網路斷線或斷電,是 commit 後才發生錯誤就真的無法避免了。
上面我們知道了 2PC 的基本原理,再來就是直接上個真實案例,以便學習真正的落地。
請事先準備好 DB,postgres 的max_prepared_transactions記得打開設定參數,否則會使用 prepare.
在 Postgres 之中,prepare statement 為PREPARE TRANSACTION transaction_id,詳細介紹可以參考官方介紹。
A 資料庫中的用戶 aa,轉 500 元到 B 資料庫用戶 bb。
需求:
| time | db A | db B |
|---|---|---|
| 1 | begin |
|
| 2 | update account set balance=balance-500 where id='aa; |
|
| 3 | PREPARE TRANSACTION 'foo'; |
|
| 4 | begin |
|
| 5 | update account set balance=balance+500 where id='bb; |
|
| 6 | PREPARE TRANSACTION 'bar'; |
|
| 6 | commit PREPARED 'foo'; / rollback PREPARED 'foo'; |
|
| 7 | commit PREPARED 'bar'; / rollback PREPARED 'bar'; |
今天加上了PREPARE TRANSACTION,就可以在 commit 時提前發現問題並且 rollback,
便免其中一方事先 commit,造成無法挽回的後果。
我們知道 2PC 的重點其實就是加上prepare statement,
那麼其實 2PC 的使用範圍就不只是 DB 了,甚至金流 api,不同類型的資料庫串接等等,
只要協調者需要維持強一致性的需求,那麼 2PC 就是一個很好的選擇。
或許各位讀者早就已經在使用 2PC 了,只是當時不知道那是 2PC 而已,
如今需多 API 等概念都使用到了 2PC。
2PC 也可以當作共識演算法,因為要讓每個節點都能達到相同的值。
其實 2PC 也是 Paxos 算法的一種簡化版本。