當使用涉及多個來源(包括循環複寫)的複寫設定時,不同的來源可能會嘗試使用不同的資料更新複本上的同一列。NDB Cluster 複寫中的衝突解決提供了解決此類衝突的方法,允許使用使用者定義的解決方案欄來決定是否應該在複本上套用給定來源的更新。
NDB Cluster 支援的某些衝突解決類型(NDB$OLD()
、NDB$MAX()
和 NDB$MAX_DELETE_WIN()
;NDB$MAX_INS()
和 NDB$MAX_DEL_WIN_INS()
)將此使用者定義的欄實作為「時間戳記」欄(儘管其類型不能為 TIMESTAMP
,如本節稍後所述)。這些類型的衝突解決始終逐列套用,而不是逐個交易套用。基於 epoch 的衝突解決函數 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
會比較 epoch 的複寫順序(因此這些函數是交易性的)。當發生衝突時,可以使用不同的方法來比較複本上的解決方案欄值,如本節稍後所述;可以使用設定的方法來作用於單個表格、資料庫或伺服器,或使用模式比對作用於一組或多個表格。如需關於在 mysql.ndb_replication
表格的 db
、table_name
和 server_id
欄中使用模式比對的資訊,請參閱使用萬用字元比對。
您還應該記住,應用程式有責任確保解決方案欄正確填入相關值,以便解決方案函數可以在決定是否套用更新時做出適當的選擇。
必須在來源和複本上進行衝突解決的準備工作。這些任務在以下列表中描述
在寫入二進制日誌的來源上,您必須決定要傳送哪些欄(所有欄或僅傳送已更新的欄)。這是透過套用 mysqld 啟動選項
--ndb-log-updated-only
(本節稍後描述)來針對整個 MySQL 伺服器完成,或者透過在mysql.ndb_replication
表格中放置適當的項目來針對一個或多個特定表格完成(請參閱 ndb_replication 表格)。注意如果您要複寫具有非常大欄的表格(例如
TEXT
或BLOB
欄),--ndb-log-updated-only
也可用於減小二進制日誌的大小,並避免因超出max_allowed_packet
而可能導致的複寫失敗。請參閱 章節 19.5.1.21,「複寫和 max_allowed_packet」,以取得關於此問題的更多資訊。
在複本上,您必須決定要套用哪種類型的衝突解決(「最新時間戳記獲勝」、「相同時間戳記獲勝」、「主要獲勝」、「主要獲勝,完整交易」或無)。這是使用
mysql.ndb_replication
系統表格完成的,並套用至一個或多個特定表格(請參閱 ndb_replication 表格)。NDB Cluster 還支援讀取衝突偵測,也就是偵測一個叢集中給定列的讀取與另一個叢集中相同列的更新或刪除之間的衝突。這需要透過在複本上將
ndb_log_exclusive_reads
設定為 1 來取得獨佔讀取鎖定。衝突讀取讀取的所有列都會記錄在例外表格中。如需更多資訊,請參閱 讀取衝突偵測和解決。當使用
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
時,NDB
可以等冪地套用WRITE_ROW
事件,在傳入的列尚不存在時將此類事件對應到插入,如果存在則對應到更新。當使用
NDB$MAX_INS()
或NDB$MAX_DEL_WIN_INS()
以外的任何衝突解決函數時,如果列已存在,則始終拒絕傳入的寫入。
當使用函數 NDB$OLD()
、NDB$MAX()
、NDB$MAX_DELETE_WIN()
、NDB$MAX_INS()
和 NDB$MAX_DEL_WIN_INS()
進行基於時間戳記的衝突解決時,我們通常將用於確定更新的欄稱為「時間戳記」欄。但是,此欄的資料類型永遠不是 TIMESTAMP
;相反地,其資料類型應為 INT
(INTEGER
)或 BIGINT
。「時間戳記」欄也應為 UNSIGNED
和 NOT NULL
。
本節稍後討論的 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
函數透過比較在主要和次要 NDB Cluster 上套用的複寫 epoch 的相對順序來工作,並且不使用時間戳記。
我們可以根據「之前」和「之後」映像來查看更新作業,也就是在套用更新之前和之後的表格狀態。通常,當使用主索引鍵更新表格時,「之前」映像不是非常受關注;但是,當我們需要根據每次更新來決定是否在複本上使用更新的值時,我們需要確保將兩個映像都寫入來源的二進制日誌。這是使用 mysqld 的 --ndb-log-update-as-write
選項完成的,如本節稍後所述。
衝突解決通常在可能發生衝突的伺服器上啟用。與記錄方法選擇類似,它是透過 mysql.ndb_replication
資料表中的條目來啟用。
NBT_UPDATED_ONLY_MINIMAL
和 NBT_UPDATED_FULL_MINIMAL
可與 NDB$EPOCH()
、NDB$EPOCH2()
和 NDB$EPOCH_TRANS()
一起使用,因為這些不需要不是主鍵的欄位的「之前」值。需要舊值的衝突解決演算法,例如 NDB$MAX()
和 NDB$OLD()
,使用這些 binlog_type
值時無法正常運作。
本節提供有關可與 NDB 複寫一起用於衝突偵測和解決的函數的詳細資訊。
NDB$OLD()
如果來源和複本上的 column_name
值相同,則會套用更新;否則,更新不會套用在複本上,並會將例外狀況寫入記錄。以下虛擬程式碼說明了這一點
if (source_old_column_value == replica_current_column_value)
apply_update();
else
log_exception();
此函數可用於「相同值勝出」的衝突解決。這種衝突解決類型可確保不會將來自錯誤來源的更新套用在複本上。
此函數使用來源的「之前」映像中的欄位值。
NDB$MAX()
對於更新或刪除操作,如果來自來源的給定資料列的「時間戳記」欄位值高於複本上的值,則會套用該操作;否則,不會將其套用在複本上。以下虛擬程式碼說明了這一點
if (source_new_column_value > replica_current_column_value)
apply_update();
此函數可用於「最大時間戳記勝出」的衝突解決。這種衝突解決類型可確保在發生衝突時,最近更新的資料列版本是持續存在的版本。
此函數對寫入操作之間的衝突沒有影響,除非先前寫入具有相同主鍵的寫入操作一律會遭到拒絕;只有在不存在使用相同主鍵的寫入操作時,才會接受並套用該寫入操作。您可以使用 NDB$MAX_INS()
來處理寫入之間的衝突解決。
此函數使用來源的「之後」映像中的欄位值。
NDB$MAX_DELETE_WIN()
這是 NDB$MAX()
的變體。由於刪除操作沒有時間戳記可用,因此使用 NDB$MAX()
的刪除實際上會以 NDB$OLD
的方式處理,但對於某些使用案例來說,這不是最佳的。對於 NDB$MAX_DELETE_WIN()
,如果來自來源的給定資料列新增或更新現有資料列的「時間戳記」欄位值高於複本上的值,則會套用該操作。但是,刪除操作會被視為一律具有較高的值。以下虛擬程式碼說明了這一點
if ( (source_new_column_value > replica_current_column_value)
||
operation.type == "delete")
apply_update();
此函數可用於「最大時間戳記,刪除勝出」的衝突解決。這種衝突解決類型可確保在發生衝突時,已刪除或(以其他方式)最近更新的資料列版本是持續存在的版本。
與 NDB$MAX()
一樣,此函數使用來源「之後」映像中的欄位值。
NDB$MAX_INS()
此函數提供對衝突寫入操作解決的支援。「NDB$MAX_INS()」會以如下方式處理此類衝突
如果沒有衝突的寫入,則套用此寫入(這與
NDB$MAX()
相同)。否則,套用「最大時間戳記勝出」的衝突解決,如下所示
如果傳入寫入的時間戳記大於衝突寫入的時間戳記,則套用傳入的操作。
如果傳入寫入的時間戳記不大於,則拒絕傳入的寫入操作。
在處理插入操作時,NDB$MAX_INS()
會比較來源和複本的時間戳記,如下列虛擬程式碼所示
if (source_new_column_value > replica_current_column_value)
apply_insert();
else
log_exception();
對於更新操作,會將來源的已更新時間戳記欄位值與複本的時間戳記欄位值進行比較,如下所示
if (source_new_column_value > replica_current_column_value)
apply_update();
else
log_exception();
這與 NDB$MAX()
執行的操作相同。
對於刪除操作,處理方式也與 NDB$MAX()
執行的操作相同(因此與 NDB$OLD()
相同),且執行方式如下
if (source_new_column_value == replica_current_column_value)
apply_delete();
else
log_exception();
NDB$MAX_DEL_WIN_INS()
此函數提供對衝突寫入操作解決的支援,以及像 NDB$MAX_DELETE_WIN()
的「刪除勝出」解決。 NDB$MAX_DEL_WIN_INS()
會按如下所示處理寫入衝突
如果沒有衝突的寫入,則套用此寫入(這與
NDB$MAX_DELETE_WIN()
相同)。否則,套用「最大時間戳記勝出」的衝突解決,如下所示
如果傳入寫入的時間戳記大於衝突寫入的時間戳記,則套用傳入的操作。
如果傳入寫入的時間戳記不大於,則拒絕傳入的寫入操作。
可以虛擬程式碼形式表示 NDB$MAX_DEL_WIN_INS()
執行的插入操作處理,如下所示
if (source_new_column_value > replica_current_column_value)
apply_insert();
else
log_exception();
對於更新操作,會將來源的已更新時間戳記欄位值與複本的時間戳記欄位值進行比較,如下所示(再次使用虛擬程式碼)
if (source_new_column_value > replica_current_column_value)
apply_update();
else
log_exception();
刪除是使用「刪除永遠勝出」策略處理(與 NDB$MAX_DELETE_WIN()
相同);一律會套用 DELETE
,而不考慮任何時間戳記值,如下列虛擬程式碼所示
if (operation.type == "delete")
apply_delete();
對於更新操作與刪除操作之間的衝突,此函數的行為與 NDB$MAX_DELETE_WIN()
完全相同。
NDB$EPOCH()
NDB$EPOCH()
函數會追蹤相對於複本上發生的變更,複寫的紀元在複本叢集上套用的順序。此相對順序是用於判斷複本上發生的變更是否與在本地發生的任何變更同時發生,因此可能存在衝突。
以下 NDB$EPOCH()
描述中的大部分內容也適用於 NDB$EPOCH_TRANS()
。任何例外情況都會在文字中註明。
NDB$EPOCH()
是非對稱的,它在雙向複寫組態(有時稱為「主動-主動」複寫)中的一個 NDB 叢集上運作。我們在這裡將其運作的叢集稱為主要叢集,而將另一個叢集稱為次要叢集。主要叢集上的複本負責偵測和處理衝突,而次要叢集上的複本不參與任何衝突偵測或處理。
當主要叢集上的複本偵測到衝突時,它會將事件插入其自己的二進位記錄中以補償這些衝突;這可確保次要 NDB 叢集最終與主要叢集重新對齊,並防止主要叢集和次要叢集發生分歧。這種補償和重新對齊機制要求主要 NDB 叢集在與次要叢集發生任何衝突時一律勝出 — 也就是說,在發生衝突時,一律使用主要叢集的變更,而非次要叢集的變更。此「主要一律勝出」規則具有以下含義
在主要叢集上認可的變更資料的操作,一旦認可便會完全持久,且不會因衝突偵測和解決而取消或回復。
從主要叢集讀取的資料是完全一致的。在主要叢集上認可的任何變更(在本機或從複本)都不會在稍後還原。
如果主要叢集判斷次要叢集上的資料變更操作發生衝突,稍後可能會還原這些操作。
在次要叢集上讀取的個別資料列在任何時候都是自我一致的,每個資料列一律反映次要叢集認可的狀態或主要叢集認可的狀態。
在次要叢集上讀取的資料列集在給定單一時點不一定一致。對於
NDB$EPOCH_TRANS()
來說,這是一個暫時狀態;對於NDB$EPOCH()
來說,這可能是一個持久狀態。假設一段時間沒有任何衝突,則次要 NDB 叢集上的所有資料(最終)會與主要叢集的資料一致。
NDB$EPOCH()
和 NDB$EPOCH_TRANS()
不需要任何使用者結構描述修改或應用程式變更來提供衝突偵測。但是,必須仔細思考使用的結構描述和使用的存取模式,以驗證完整系統是否在指定的限制內運作。
每個 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
函數都可以採用選用參數;這是用於表示紀元較低 32 個位元的位元數,且應設定為不小於如下所示計算的值
CEIL( LOG2( TimeBetweenGlobalCheckpoints / TimeBetweenEpochs ), 1)
對於這些組態參數的預設值(分別為 2000 和 100 毫秒),這會得出 5 個位元的值,因此預設值 (6) 應該足夠,除非 TimeBetweenGlobalCheckpoints
、TimeBetweenEpochs
或兩者都使用其他值。過小的值可能會導致誤報,而過大的值可能會導致資料庫中浪費過多空間。
NDB$EPOCH()
和 NDB$EPOCH_TRANS()
都會將衝突列的條目插入相關的例外表格中,前提是這些表格已按照本節其他部分所述的相同例外表格結構規則定義(請參閱 NDB$OLD())。您必須先建立任何例外表格,然後才能建立要與之使用的資料表。
與本節討論的其他衝突偵測函式一樣,NDB$EPOCH()
和 NDB$EPOCH_TRANS()
是透過在 mysql.ndb_replication
表格中包含相關條目來啟動的(請參閱 ndb_replication 表格)。在此情境中,主要和次要 NDB 叢集的角色完全由 mysql.ndb_replication
表格條目決定。
由於 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
採用的衝突偵測演算法是不對稱的,因此您必須對主要和次要複本的 server_id
條目使用不同的值。
僅僅是 DELETE
作業之間的衝突不足以使用 NDB$EPOCH()
或 NDB$EPOCH_TRANS()
來觸發衝突,並且在 epoch 內部的相對位置並不重要。
目前在使用 NDB$EPOCH()
執行衝突偵測時,適用下列限制
衝突是使用 NDB 叢集 epoch 邊界偵測的,其粒度與
TimeBetweenEpochs
成正比(預設值:100 毫秒)。最小衝突視窗是同時更新兩個叢集上相同資料,且總是會回報衝突的最短時間。這永遠是一個非零的時間長度,且大致與2 * (延遲 + 排隊 + TimeBetweenEpochs)
成正比。這表示,假設TimeBetweenEpochs
的預設值,並忽略叢集之間的任何延遲(以及任何排隊延遲),則最小衝突視窗大小約為 200 毫秒。在查看預期的應用程式「競爭」模式時,應考慮此最小視窗。使用
NDB$EPOCH()
和NDB$EPOCH_TRANS()
函式的表格需要額外的儲存空間;每個列需要 1 到 32 位元的額外空間,具體取決於傳遞給函式的值。刪除作業之間的衝突可能會導致主要和次要之間的差異。當在兩個叢集上同時刪除一列時,可以偵測到衝突,但由於該列已刪除,因此不會記錄衝突。這表示在傳播任何後續重新對齊作業期間,不會偵測到進一步的衝突,這可能會導致差異。
刪除應該在外部序列化,或僅路由到一個叢集。或者,應該使用這些刪除及其後的任何插入來以交易方式更新單獨的列,以便可以追蹤跨列刪除的衝突。這可能需要變更應用程式。
當使用
NDB$EPOCH()
或NDB$EPOCH_TRANS()
進行衝突偵測時,目前僅支援雙向「主動-主動」組態中的兩個 NDB 叢集。具有
BLOB
或TEXT
資料行的表格目前不支援使用NDB$EPOCH()
或NDB$EPOCH_TRANS()
。
NDB$EPOCH_TRANS()
NDB$EPOCH_TRANS()
擴充了 NDB$EPOCH()
函式。使用「主要勝出所有」規則(請參閱 NDB$EPOCH())以相同的方式偵測和處理衝突,但額外條件是在發生衝突的同一個交易中更新的任何其他列,也會被視為發生衝突。換句話說,NDB$EPOCH()
在次要節點上重新對齊個別衝突列,而 NDB$EPOCH_TRANS()
則重新對齊衝突交易。
此外,任何可偵測到依賴衝突交易的交易,也會被視為發生衝突,這些依賴關係是由次要叢集的二進位日誌內容決定的。由於二進位日誌僅包含資料修改作業(插入、更新和刪除),因此只有重疊的資料修改會用於判斷交易之間的依賴關係。
NDB$EPOCH_TRANS()
與 NDB$EPOCH()
具有相同的條件和限制,此外,還要求所有交易 ID 都記錄在次要節點的二進位日誌中,並將 --ndb-log-transaction-id
設定為 ON
。這會增加可變的負擔(每個列最多 13 個位元組)。
請參閱 NDB$EPOCH()。
NDB$EPOCH2()
NDB$EPOCH2()
函式與 NDB$EPOCH()
類似,但 NDB$EPOCH2()
提供具有雙向複寫拓樸的刪除-刪除處理。在此情境中,透過在每個來源上將 ndb_conflict_role
系統變數設定為適當的值(通常每個來源一個 PRIMARY
、SECONDARY
),將主要和次要角色指派給兩個來源。完成此操作後,次要節點所做的修改會由主要節點反映回次要節點,然後次要節點會條件式地套用這些修改。
NDB$EPOCH2_TRANS()
NDB$EPOCH2_TRANS()
擴充了 NDB$EPOCH2()
函式。以相同的方式偵測和處理衝突,並將主要和次要角色指派給複寫叢集,但額外條件是在發生衝突的同一個交易中更新的任何其他列,也會被視為發生衝突。也就是說,NDB$EPOCH2()
會重新對齊次要節點上的個別衝突列,而 NDB$EPOCH_TRANS()
則會重新對齊衝突交易。
當 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
使用每個列、每個上次修改的 epoch 指定的中繼資料,以在主要節點上判斷來自次要節點的傳入複寫列變更是否與本機提交的變更同時發生時;同時發生的變更會被視為衝突,並隨後更新例外表格並重新對齊次要節點。當在主要節點上刪除一列時,就會出現問題,因為不再有任何上次修改的 epoch 可用來判斷任何複寫作業是否衝突,這表示不會偵測到衝突的刪除作業。這可能會導致差異,例如,在一個叢集上的刪除與在另一個叢集上的刪除和插入同時發生;這就是為什麼在使用 NDB$EPOCH()
和 NDB$EPOCH_TRANS()
時,刪除作業只能路由到一個叢集的原因。
NDB$EPOCH2()
會略過剛剛描述的問題,將關於主要節點上已刪除列的資訊儲存起來,方法是忽略任何刪除-刪除衝突,並避免任何潛在的結果差異。這是透過將從次要節點成功套用和複寫的任何作業反映回次要節點來完成的。當返回次要節點時,可用於在次要節點上重新套用被來自主要節點的作業刪除的作業。
使用 NDB$EPOCH2()
時,您應該記住,次要節點會套用來自主要節點的刪除,直到透過反映的作業還原新列。理論上,次要節點上後續的插入或更新會與來自主要節點的刪除衝突,但在這種情況下,為了防止叢集之間出現差異,我們選擇忽略此衝突,並允許次要節點「勝出」。換句話說,在刪除之後,主要節點不會偵測到衝突,而是立即採用次要節點的後續變更。因此,次要節點的狀態可能會在進展到最終(穩定)狀態時,多次回顧先前提交的狀態,而且可能會看到其中一些狀態。
您也應該注意到,將所有作業從次要節點反映回主要節點,會增加主要節點日誌二進位日誌的大小,並增加對頻寬、CPU 使用率和磁碟 I/O 的需求。
次要節點上反映操作的應用取決於次要節點上目標列的狀態。是否在次要節點上應用反映的變更,可以透過檢查 Ndb_conflict_reflected_op_prepare_count
和 Ndb_conflict_reflected_op_discard_count
狀態變數,或效能架構 (Performance Schema) ndb_replication_applier_status
資料表的 CONFLICT_REFLECTED_OP_PREPARE_COUNT
和 CONFLICT_REFLECTED_OP_DISCARD_COUNT
欄位來追蹤。應用變更的數量只是這兩個值之間的差異(請注意,Ndb_conflict_reflected_op_prepare_count
始終大於或等於 Ndb_conflict_reflected_op_discard_count
)。
只有當以下兩個條件都為真時,才會套用事件
列的存在,也就是說,它是否存在,與事件的類型一致。對於刪除和更新操作,該列必須已經存在。對於插入操作,該列必須不存在。
該列上次是由主要節點修改的。修改可能透過執行反映操作來完成。
如果這兩個條件都不符合,則次要節點會捨棄反映的操作。
若要使用 NDB$OLD()
衝突解決函式,還必須為每個要使用此類型衝突解決的 NDB
表格建立對應的例外表格。當使用 NDB$EPOCH()
或 NDB$EPOCH_TRANS()
時也是如此。此表格的名稱是將要應用衝突解決的表格的名稱,並附加字串 $EX
。(例如,如果原始表格的名稱為 mytable
,則對應的例外表格名稱應為 mytable$EX
。)建立例外表格的語法如下所示
CREATE TABLE original_table$EX (
[NDB$]server_id INT UNSIGNED,
[NDB$]source_server_id INT UNSIGNED,
[NDB$]source_epoch BIGINT UNSIGNED,
[NDB$]count INT UNSIGNED,
[NDB$OP_TYPE ENUM('WRITE_ROW','UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,]
[NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,]
[NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL,]
original_table_pk_columns,
[orig_table_column|orig_table_column$OLD|orig_table_column$NEW,]
[additional_columns,]
PRIMARY KEY([NDB$]server_id, [NDB$]source_server_id, [NDB$]source_epoch, [NDB$]count)
) ENGINE=NDB;
前四個欄位是必要的。前四個欄位的名稱以及與原始表格主鍵欄位匹配的欄位並不重要;但是,為了清楚和一致起見,我們建議您使用此處顯示的 server_id
、source_server_id
、source_epoch
和 count
欄位的名稱,並對與原始表格主鍵中的欄位匹配的欄位使用與原始表格中相同的名稱。
如果例外表格使用本節稍後討論的一或多個可選欄位 NDB$OP_TYPE
、NDB$CFT_CAUSE
或 NDB$ORIG_TRANSID
,則每個必要的欄位也都必須使用前綴 NDB$
來命名。如果需要,即使您沒有定義任何可選欄位,也可以使用 NDB$
前綴來命名必要的欄位,但在這種情況下,所有四個必要的欄位都必須使用前綴命名。
在這些欄位之後,應按用於定義原始表格主鍵的順序複製組成原始表格主鍵的欄位。重複原始表格主鍵欄位的欄位的資料類型應與原始欄位的資料類型相同(或更大)。可以使用主鍵欄位的一個子集。
例外表格必須使用 NDB
儲存引擎。(本節稍後會顯示使用 NDB$OLD()
和例外表格的範例。)
可以在複製的主鍵欄位之後選擇性地定義其他欄位,但不得在任何主鍵欄位之前;任何此類額外的欄位都不能為 NOT NULL
。NDB Cluster 支援三個額外的、預先定義的可選欄位 NDB$OP_TYPE
、NDB$CFT_CAUSE
和 NDB$ORIG_TRANSID
,這些欄位將在接下來的幾個段落中描述。
NDB$OP_TYPE
:此欄位可用於取得造成衝突的操作類型。如果您使用此欄位,請如下所示定義它
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL
WRITE_ROW
、UPDATE_ROW
和 DELETE_ROW
操作類型代表使用者起始的操作。REFRESH_ROW
操作是由衝突解決在補償交易中產生的操作,這些操作從偵測到衝突的叢集傳送回原始叢集。READ_ROW
操作是使用獨佔列鎖定定義的使用者起始的讀取追蹤操作。
NDB$CFT_CAUSE
:您可以定義一個可選欄位 NDB$CFT_CAUSE
,它提供已註冊衝突的原因。如果使用此欄位,則按如下所示定義
NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL
ROW_DOES_NOT_EXIST
可以作為 UPDATE_ROW
和 WRITE_ROW
操作的原因回報;ROW_ALREADY_EXISTS
可以針對 WRITE_ROW
事件回報。當基於列的衝突函式偵測到衝突時,會回報 DATA_IN_CONFLICT
;當交易衝突函式拒絕屬於完整交易的所有操作時,會回報 TRANS_IN_CONFLICT
。
NDB$ORIG_TRANSID
:NDB$ORIG_TRANSID
欄位(如果使用)包含原始交易的 ID。此欄位應定義如下
NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL
NDB$ORIG_TRANSID
是由 NDB
產生的 64 位元值。此值可用於關聯屬於來自相同或不同例外表格的相同衝突交易的多個例外表格條目。
不屬於原始表格主鍵的其他參考欄位可以命名為
或 colname
$OLD
。colname
$NEW
參考更新和刪除操作中的舊值,也就是說,包含 colname
$OLDDELETE_ROW
事件的操作。
可用於參考插入和更新操作中的新值,換句話說,也就是使用 colname
$NEWWRITE_ROW
事件、UPDATE_ROW
事件或兩種事件類型的操作。如果衝突操作沒有為給定參考欄位(不是主鍵)提供值,則例外表格列會包含 NULL
或該欄位的定義的預設值。
當設定資料表格以進行複寫時,會讀取 mysql.ndb_replication
表格,因此必須在建立要複寫的表格之前,將對應於要複寫的表格的列插入到 mysql.ndb_replication
中。
可以使用多個狀態變數來監控衝突偵測。您可以透過 Ndb_conflict_fn_epoch
系統狀態變數的目前值,或檢查效能架構 (Performance Schema) ndb_replication_applier_status
表格的 CONFLICT_FN_EPOCH
欄位,查看自上次從此複本重新啟動以來,NDB$EPOCH()
中發現有多少列存在衝突。
Ndb_conflict_fn_epoch_trans
提供透過 NDB$EPOCH_TRANS()
直接發現存在衝突的列數。Ndb_conflict_fn_epoch2
和 Ndb_conflict_fn_epoch2_trans
分別顯示透過 NDB$EPOCH2()
和 NDB$EPOCH2_TRANS()
發現存在衝突的列數。實際重新對齊的列數,包括因其在與其他衝突列相同的交易中或對這些交易的依賴而受影響的列數,由 Ndb_conflict_trans_row_reject_count
提供。
另一個伺服器狀態變數 Ndb_conflict_fn_max
提供自上次啟動 mysqld 以來,由於「最大時間戳記獲勝」衝突解決,目前 SQL 節點上未套用列的次數。Ndb_conflict_fn_max_del_win
提供基於 NDB$MAX_DELETE_WIN()
的結果而應用衝突解決的次數。
Ndb_conflict_fn_max_ins
追蹤「較大時間戳記獲勝」處理已應用於寫入操作(使用 NDB$MAX_INS()
)的次數;狀態變數 Ndb_conflict_fn_max_del_win_ins
提供「相同時間戳記獲勝」處理寫入的次數(如 NDB$MAX_DEL_WIN_INS()
所實作)。
自上次重新啟動以來,由於給定的 mysqld 上「相同時間戳記獲勝」衝突解決而未應用列的次數由全域狀態變數 Ndb_conflict_fn_old
提供。除了遞增 Ndb_conflict_fn_old
之外,未使用的列的主鍵會插入到例外表格中,如本節其他地方所述。
前面段落中參考的每個狀態變數在效能架構 (Performance Schema) ndb_replication_applier_status
表格中都有一個等效欄位。有關更多資訊,請參閱此表格的描述。另請參閱第 25.4.3.9.3 節,「NDB Cluster 狀態變數」。
以下範例假設您已經有一個可運作的 NDB Cluster 複寫設定,如第 25.7.5 節,「準備 NDB Cluster 進行複寫」和第 25.7.6 節,「啟動 NDB Cluster 複寫(單一複寫通道)」中所述。
NDB$MAX() 範例。假設您想要在表格 test.t1
上啟用「最大時間戳記獲勝」衝突解決,並使用欄位 mycol
作為「時間戳記」。這可以使用以下步驟完成
確保您已使用
--ndb-log-update-as-write=OFF
啟動來源 mysqld。在來源上,執行此
INSERT
陳述式INSERT INTO mysql.ndb_replication VALUES ('test', 't1', 0, NULL, 'NDB$MAX(mycol)');
注意如果
ndb_replication
表格尚不存在,您必須建立它。請參閱ndb_replication 表格。在
server_id
欄位中插入 0 表示所有存取此資料表的 SQL 節點都應使用衝突解決。如果您只想在特定的 mysqld 上使用衝突解決,請使用實際的伺服器 ID。在
binlog_type
欄位中插入NULL
的效果與插入 0 (NBT_DEFAULT
) 相同;將會使用伺服器的預設值。建立
test.t1
資料表CREATE TABLE test.t1 ( columns mycol INT UNSIGNED, columns ) ENGINE=NDB;
現在,當對此資料表執行更新時,會套用衝突解決,並且將
mycol
值最大的資料列版本寫入複本。
其他 binlog_type
選項,例如 NBT_UPDATED_ONLY_USE_UPDATE
(6
),應該使用 ndb_replication
資料表來控制來源上的記錄,而不是使用命令列選項。
NDB$OLD() 範例。假設正在複製一個 NDB
資料表,例如此處定義的資料表,並且您希望針對此資料表的更新啟用「相同時間戳記獲勝」衝突解決。
CREATE TABLE test.t2 (
a INT UNSIGNED NOT NULL,
b CHAR(25) NOT NULL,
columns,
mycol INT UNSIGNED NOT NULL,
columns,
PRIMARY KEY pk (a, b)
) ENGINE=NDB;
以下步驟是必須的,並按照所示順序執行
首先 — 且在建立
test.t2
之前 — 您必須在mysql.ndb_replication
資料表中插入一個資料列,如下所示INSERT INTO mysql.ndb_replication VALUES ('test', 't2', 0, 0, 'NDB$OLD(mycol)');
binlog_type
欄位的可能值已在本節前面顯示;在此案例中,我們使用0
來指定應使用伺服器預設記錄行為。值'NDB$OLD(mycol)'
應插入conflict_fn
欄位。為
test.t2
建立適當的例外狀況資料表。此處顯示的資料表建立語句包含所有必要的欄位;任何其他欄位都必須在此些欄位之後,以及資料表主鍵定義之前宣告。CREATE TABLE test.t2$EX ( server_id INT UNSIGNED, source_server_id INT UNSIGNED, source_epoch BIGINT UNSIGNED, count INT UNSIGNED, a INT UNSIGNED NOT NULL, b CHAR(25) NOT NULL, [additional_columns,] PRIMARY KEY(server_id, source_server_id, source_epoch, count) ) ENGINE=NDB;
我們可以包含額外的欄位,以提供有關給定衝突的類型、原因和原始交易 ID 的資訊。我們也不需要為原始資料表中的所有主鍵欄位提供相符的欄位。這表示您可以像這樣建立例外狀況資料表
CREATE TABLE test.t2$EX ( NDB$server_id INT UNSIGNED, NDB$source_server_id INT UNSIGNED, NDB$source_epoch BIGINT UNSIGNED, NDB$count INT UNSIGNED, a INT UNSIGNED NOT NULL, NDB$OP_TYPE ENUM('WRITE_ROW','UPDATE_ROW', 'DELETE_ROW', 'REFRESH_ROW', 'READ_ROW') NOT NULL, NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS', 'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL, NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL, [additional_columns,] PRIMARY KEY(NDB$server_id, NDB$source_server_id, NDB$source_epoch, NDB$count) ) ENGINE=NDB;
注意因為我們在資料表定義中至少包含欄位
NDB$OP_TYPE
、NDB$CFT_CAUSE
或NDB$ORIG_TRANSID
其中一個,因此四個必要的欄位都需要NDB$
前綴。如先前所示,建立資料表
test.t2
。
對於您希望使用 NDB$OLD()
執行衝突解決的每個資料表,都必須遵循這些步驟。對於每個此類資料表,在 mysql.ndb_replication
中都必須有一個對應的資料列,並且在與被複製的資料表相同的資料庫中必須有一個例外狀況資料表。
讀取衝突偵測和解決。 NDB Cluster 也支援追蹤讀取操作,這使得在循環複製設定中,可以管理一個叢集中給定資料列的讀取與另一個叢集中相同資料列的更新或刪除之間的衝突。此範例使用 employee
和 department
資料表來模擬一種情況,其中員工從來源叢集(我們在下文中稱為叢集 A)的一個部門移至另一個部門,而複本叢集(下文中稱為 B)在交錯的交易中更新員工先前部門的員工計數。
資料表已使用以下 SQL 語句建立
# Employee table
CREATE TABLE employee (
id INT PRIMARY KEY,
name VARCHAR(2000),
dept INT NOT NULL
) ENGINE=NDB;
# Department table
CREATE TABLE department (
id INT PRIMARY KEY,
name VARCHAR(2000),
members INT
) ENGINE=NDB;
兩個資料表的內容包括以下 SELECT
語句(部分)輸出的資料列
mysql> SELECT id, name, dept FROM employee;
+---------------+------+
| id | name | dept |
+------+--------+------+
...
| 998 | Mike | 3 |
| 999 | Joe | 3 |
| 1000 | Mary | 3 |
...
+------+--------+------+
mysql> SELECT id, name, members FROM department;
+-----+-------------+---------+
| id | name | members |
+-----+-------------+---------+
...
| 3 | Old project | 24 |
...
+-----+-------------+---------+
我們假設我們已經在使用一個例外狀況資料表,該資料表包含四個必要的欄位(這些欄位用於此資料表的主鍵)、操作類型和原因的可選欄位,以及原始資料表的主鍵欄位,並且使用此處顯示的 SQL 語句建立
CREATE TABLE employee$EX (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM( 'WRITE_ROW','UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW','READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM( 'ROW_DOES_NOT_EXIST',
'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT',
'TRANS_IN_CONFLICT') NOT NULL,
id INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id, NDB$source_epoch, NDB$count)
) ENGINE=NDB;
假設在兩個叢集上發生兩個同時的交易。在叢集 A 上,我們建立一個新的部門,然後使用以下 SQL 語句將員工編號 999 移至該部門
BEGIN;
INSERT INTO department VALUES (4, "New project", 1);
UPDATE employee SET dept = 4 WHERE id = 999;
COMMIT;
同時,在叢集 B 上,另一個交易從 employee
讀取,如下所示
BEGIN;
SELECT name FROM employee WHERE id = 999;
UPDATE department SET members = members - 1 WHERE id = 3;
commit;
衝突交易通常不會被衝突解決機制偵測到,因為衝突發生在讀取(SELECT
)和更新操作之間。您可以透過在複本叢集上執行 SET
ndb_log_exclusive_reads
= 1
來規避此問題。以這種方式取得獨佔讀取鎖定會導致在來源上讀取的任何資料列被標記為需要在複本叢集上進行衝突解決。如果我們在記錄這些交易之前以這種方式啟用獨佔讀取,則叢集 B 上的讀取會被追蹤並傳送至叢集 A 進行解決;隨後會偵測到員工資料列上的衝突,並且叢集 B 上的交易會被中止。
衝突會在例外狀況資料表(位於叢集 A 上)中註冊為 READ_ROW
操作(有關操作類型的描述,請參閱 衝突解決例外狀況資料表),如下所示
mysql> SELECT id, NDB$OP_TYPE, NDB$CFT_CAUSE FROM employee$EX;
+-------+-------------+-------------------+
| id | NDB$OP_TYPE | NDB$CFT_CAUSE |
+-------+-------------+-------------------+
...
| 999 | READ_ROW | TRANS_IN_CONFLICT |
+-------+-------------+-------------------+
任何在讀取操作中找到的現有資料列都會被標記。這表示可能會在例外狀況資料表中記錄同一衝突產生的多個資料列,如檢查在同時交易中,叢集 A 上的更新與叢集 B 上從同一資料表讀取多個資料列之間的衝突效果所示。在叢集 A 上執行的交易如下所示
BEGIN;
INSERT INTO department VALUES (4, "New project", 0);
UPDATE employee SET dept = 4 WHERE dept = 3;
SELECT COUNT(*) INTO @count FROM employee WHERE dept = 4;
UPDATE department SET members = @count WHERE id = 4;
COMMIT;
同時,在叢集 B 上執行包含以下語句的交易
SET ndb_log_exclusive_reads = 1; # Must be set if not already enabled
...
BEGIN;
SELECT COUNT(*) INTO @count FROM employee WHERE dept = 3 FOR UPDATE;
UPDATE department SET members = @count WHERE id = 3;
COMMIT;
在此案例中,讀取與第二個交易的 SELECT
中 WHERE
條件相符的所有三個資料列,因此會在例外狀況資料表中標記,如下所示
mysql> SELECT id, NDB$OP_TYPE, NDB$CFT_CAUSE FROM employee$EX;
+-------+-------------+-------------------+
| id | NDB$OP_TYPE | NDB$CFT_CAUSE |
+-------+-------------+-------------------+
...
| 998 | READ_ROW | TRANS_IN_CONFLICT |
| 999 | READ_ROW | TRANS_IN_CONFLICT |
| 1000 | READ_ROW | TRANS_IN_CONFLICT |
...
+-------+-------------+-------------------+
讀取追蹤僅根據現有資料列執行。基於給定條件的讀取只會追蹤 找到 的任何資料列的衝突,而不會追蹤在交錯交易中插入的任何資料列的衝突。這類似於在 NDB Cluster 的單個執行個體中執行獨佔資料列鎖定的方式。
插入衝突偵測和解決範例。以下範例說明插入衝突偵測函數的使用。我們假設我們正在複製資料庫 test
中的兩個資料表 t1
和 t2
,並且我們希望將 NDB$MAX_INS()
用於 t1
,並將 NDB$MAX_DEL_WIN_INS()
用於 t2
,來使用插入衝突偵測。兩個資料表在設定流程中稍後才會建立。
設定插入衝突解決類似於設定先前範例中顯示的其他衝突偵測和解決演算法。如果用於設定二進位記錄和衝突解決的 mysql.ndb_replication
資料表尚不存在,則首先需要建立它,如下所示
CREATE TABLE mysql.ndb_replication (
db VARBINARY(63),
table_name VARBINARY(63),
server_id INT UNSIGNED,
binlog_type INT UNSIGNED,
conflict_fn VARBINARY(128),
PRIMARY KEY USING HASH (db, table_name, server_id)
) ENGINE=NDB
PARTITION BY KEY(db,table_name);
ndb_replication
資料表以每個資料表為基礎運作;也就是說,我們需要插入一個包含資料表資訊、binlog_type
值、要使用的衝突解決函數,以及每個要設定的資料表的時間戳記欄位 (X
) 名稱的資料列,如下所示
INSERT INTO mysql.ndb_replication VALUES ("test", "t1", 0, 7, "NDB$MAX_INS(X)");
INSERT INTO mysql.ndb_replication VALUES ("test", "t2", 0, 7, "NDB$MAX_DEL_WIN_INS(X)");
在這裡,我們將 binlog_type 設定為 NBT_FULL_USE_UPDATE
(7
),這表示始終會記錄完整資料列。有關其他可能的值,請參閱 ndb_replication 資料表。
您也可以為每個要使用衝突解決的 NDB
資料表建立對應的例外狀況資料表。例外狀況資料表會記錄給定資料表的衝突解決函數拒絕的所有資料列。可以使用以下兩個 SQL 語句建立用於資料表 t1
和 t2
的複製衝突偵測例外狀況資料表
CREATE TABLE `t1$EX` (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM('ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,
a INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id,
NDB$source_epoch, NDB$count)
) ENGINE=NDB;
CREATE TABLE `t2$EX` (
NDB$server_id INT UNSIGNED,
NDB$source_server_id INT UNSIGNED,
NDB$source_epoch BIGINT UNSIGNED,
NDB$count INT UNSIGNED,
NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
'REFRESH_ROW', 'READ_ROW') NOT NULL,
NDB$CFT_CAUSE ENUM( 'ROW_DOES_NOT_EXIST', 'ROW_ALREADY_EXISTS',
'DATA_IN_CONFLICT', 'TRANS_IN_CONFLICT') NOT NULL,
a INT NOT NULL,
PRIMARY KEY(NDB$server_id, NDB$source_server_id,
NDB$source_epoch, NDB$count)
) ENGINE=NDB;
最後,在建立剛才顯示的例外狀況資料表之後,您可以使用以下兩個 SQL 語句建立要複製且受衝突解決控制的資料表
CREATE TABLE t1 (
a INT PRIMARY KEY,
b VARCHAR(32),
X INT UNSIGNED
) ENGINE=NDB;
CREATE TABLE t2 (
a INT PRIMARY KEY,
b VARCHAR(32),
X INT UNSIGNED
) ENGINE=NDB;
對於每個資料表,X
欄位都用作時間戳記欄位。
一旦在來源上建立,就會複製 t1
和 t2
,並且可以假設它們同時存在於來源和複本上。在本範例的其餘部分中,我們使用 mysqlS>
來表示連接到來源的 mysql 用戶端,並使用 mysqlR>
來表示在複本上執行的 mysql 用戶端。
首先,我們在來源上的資料表中各插入一個資料列,如下所示
mysqlS> INSERT INTO t1 VALUES (1, 'Initial X=1', 1);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (1, 'Initial X=1', 1);
Query OK, 1 row affected (0.01 sec)
我們可以確定這兩個資料列在複製時不會產生任何衝突,因為在來源上發出 INSERT
語句之前,複本上的資料表不包含任何資料列。我們可以透過如下所示的在複本上選取資料表來驗證此情況
mysqlR> TABLE t1 ORDER BY a;
+---+-------------+------+
| a | b | X |
+---+-------------+------+
| 1 | Initial X=1 | 1 |
+---+-------------+------+
1 row in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+-------------+------+
| a | b | X |
+---+-------------+------+
| 1 | Initial X=1 | 1 |
+---+-------------+------+
1 row in set (0.00 sec)
接下來,我們將新的資料列插入複本上的資料表,如下所示
mysqlR> INSERT INTO t1 VALUES (2, 'Replica X=2', 2);
Query OK, 1 row affected (0.01 sec)
mysqlR> INSERT INTO t2 VALUES (2, 'Replica X=2', 2);
Query OK, 1 row affected (0.01 sec)
現在,我們將具有較大時間戳記 (X
) 欄位值的衝突資料列插入來源上的資料表,並使用此處顯示的語句
mysqlS> INSERT INTO t1 VALUES (2, 'Replica X=20', 20);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (2, 'Replica X=20', 20);
Query OK, 1 row affected (0.01 sec)
現在,我們透過(再次)從複本上的兩個資料表中選取來觀察結果,如下所示
mysqlR> TABLE t1 ORDER BY a;
+---+-------------+-------+
| a | b | X |
+---+-------------+-------+
| 1 | Initial X=1 | 1 |
+---+-------------+-------+
| 2 | Source X=20 | 20 |
+---+-------------+-------+
2 rows in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+-------------+-------+
| a | b | X |
+---+-------------+-------+
| 1 | Initial X=1 | 1 |
+---+-------------+-------+
| 1 | Source X=20 | 20 |
+---+-------------+-------+
2 rows in set (0.00 sec)
在來源上插入的資料列,其時間戳記大於複本上衝突資料列的時間戳記,已取代這些資料列。接下來,在複本上,我們插入兩個新的資料列,這些資料列與 t1
或 t2
中任何現有的資料列都沒有衝突,如下所示
mysqlR> INSERT INTO t1 VALUES (3, 'Replica X=30', 30);
Query OK, 1 row affected (0.01 sec)
mysqlR> INSERT INTO t2 VALUES (3, 'Replica X=30', 30);
Query OK, 1 row affected (0.01 sec)
在來源上插入更多具有相同主鍵值 (3
) 的資料列,會像以前一樣產生衝突,但這次我們使用時間戳記欄位的值小於複本上衝突資料列中的相同欄位值。
mysqlS> INSERT INTO t1 VALUES (3, 'Source X=3', 3);
Query OK, 1 row affected (0.01 sec)
mysqlS> INSERT INTO t2 VALUES (3, 'Source X=3', 3);
Query OK, 1 row affected (0.01 sec)
我們可以透過查詢資料表來看,來源的兩個插入都被複本拒絕,並且先前在複本上插入的資料列沒有被覆寫,如下所示在複本上的 mysql 用戶端中
mysqlR> TABLE t1 ORDER BY a;
+---+--------------+-------+
| a | b | X |
+---+--------------+-------+
| 1 | Initial X=1 | 1 |
+---+--------------+-------+
| 2 | Source X=20 | 20 |
+---+--------------+-------+
| 3 | Replica X=30 | 30 |
+---+--------------+-------+
3 rows in set (0.00 sec)
mysqlR> TABLE t2 ORDER BY a;
+---+--------------+-------+
| a | b | X |
+---+--------------+-------+
| 1 | Initial X=1 | 1 |
+---+--------------+-------+
| 2 | Source X=20 | 20 |
+---+--------------+-------+
| 3 | Replica X=30 | 30 |
+---+--------------+-------+
3 rows in set (0.00 sec)
您可以在例外狀況資料表中看到有關被拒絕的資料列的資訊,如下所示
mysqlR> SELECT NDB$server_id, NDB$source_server_id, NDB$count,
> NDB$OP_TYPE, NDB$CFT_CAUSE, a
> FROM t1$EX
> ORDER BY NDB$count\G
*************************** 1. row ***************************
NDB$server_id : 2
NDB$source_server_id: 1
NDB$count : 1
NDB$OP_TYPE : WRITE_ROW
NDB$CFT_CAUSE : DATA_IN_CONFLICT
a : 3
1 row in set (0.00 sec)
mysqlR> SELECT NDB$server_id, NDB$source_server_id, NDB$count,
> NDB$OP_TYPE, NDB$CFT_CAUSE, a
> FROM t2$EX
> ORDER BY NDB$count\G
*************************** 1. row ***************************
NDB$server_id : 2
NDB$source_server_id: 1
NDB$count : 1
NDB$OP_TYPE : WRITE_ROW
NDB$CFT_CAUSE : DATA_IN_CONFLICT
a : 3
1 row in set (0.00 sec)
正如我們之前所見,來源上沒有其他插入的資料列被複本拒絕,只有那些時間戳記值小於複本上衝突資料列的時間戳記值的資料列才會被拒絕。