文件首頁
MySQL 8.4 參考手冊
相關文件 下載本手冊
PDF (US Ltr) - 39.9Mb
PDF (A4) - 40.0Mb
Man Pages (TGZ) - 258.5Kb
Man Pages (Zip) - 365.5Kb
Info (Gzip) - 4.0Mb
Info (Zip) - 4.0Mb


MySQL 8.4 參考手冊  /  ...  /  NDB Cluster 複製衝突解決

25.7.12 NDB Cluster 複製衝突解決

當使用涉及多個來源(包括循環複製)的複製設定時,不同的來源可能會嘗試使用不同的資料更新副本上的同一列。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 表格的 dbtable_nameserver_id 欄位中使用模式匹配的相關資訊,請參閱使用萬用字元匹配

您還應記住,應用程式有責任確保解決方案欄位正確填入相關值,以便解決方案函式在決定是否套用更新時能夠做出適當的選擇。

需求

必須在來源和副本上進行衝突解決的準備工作。這些任務在以下清單中描述

  • 在寫入二進制日誌的來源上,您必須決定要傳送哪些欄位(所有欄位或僅限已更新的欄位)。這是透過套用 mysqld 啟動選項 --ndb-log-updated-only (如本節稍後所述) 或透過在 mysql.ndb_replication 表格中放置適當的條目 (請參閱 ndb_replication 表格) 為整個 MySQL Server 完成。

    注意

    如果您要複製具有非常大的欄位(例如 TEXTBLOB 欄位)的表格,--ndb-log-updated-only 也可用於減少二進制日誌的大小,並避免由於超過 max_allowed_packet 而可能發生的複製失敗。

    如需有關此問題的詳細資訊,請參閱第 19.5.1.20 節:「複製和 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時間戳記欄位也應為 UNSIGNEDNOT NULL

本節稍後討論的 NDB$EPOCH()NDB$EPOCH_TRANS() 函式透過比較在主要和次要 NDB Cluster 上套用的複製 epoch 的相對順序來工作,並且不使用時間戳記。

來源欄位控制

我們可以根據之前之後的映像來查看更新操作,也就是說,表格在套用更新之前和之後的狀態。通常,當使用主索引鍵更新表格時,之前的映像並不是非常重要;但是,當我們需要根據每次更新來決定是否要在副本上使用更新的值時,我們需要確保兩個映像都寫入來源的二進制日誌。這是使用 mysqld--ndb-log-update-as-write 選項完成,如本節稍後所述。

重要

記錄完整列還是僅記錄已更新的欄位是在 MySQL 伺服器啟動時決定的,並且無法在線上變更;您必須重新啟動 mysqld,或使用不同的記錄選項啟動新的 mysqld 執行個體。

衝突解決控制

衝突解決通常會在可能發生衝突的伺服器上啟用。與記錄方法選擇一樣,它是透過 mysql.ndb_replication 表格中的條目啟用的。

NBT_UPDATED_ONLY_MINIMALNBT_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() 按如下方式處理

  1. 如果沒有衝突的寫入,則套用此寫入(與 NDB$MAX() 相同)。

  2. 否則,套用 最大時間戳記優先 衝突解決,如下所示

    1. 如果傳入寫入的時間戳記大於衝突寫入的時間戳記,則套用傳入的操作。

    2. 如果傳入寫入的時間戳記大,則拒絕傳入的寫入操作。

在處理插入操作時,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() 按如下所示處理寫入衝突

  1. 如果沒有衝突的寫入,則套用此寫入(與 NDB$MAX_DELETE_WIN() 相同)。

  2. 否則,套用 最大時間戳記優先 衝突解決,如下所示

    1. 如果傳入寫入的時間戳記大於衝突寫入的時間戳記,則套用傳入的操作。

    2. 如果傳入寫入的時間戳記大,則拒絕傳入的寫入操作。

可以用如下所示的虛擬碼表示由 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) 應該足夠,除非將其他值用於 TimeBetweenGlobalCheckpointsTimeBetweenEpochs 或兩者。值太小可能會導致誤判,而值太大可能會導致資料庫中浪費過多的空間。

如果已根據本節中其他地方描述的相同例外狀況表結構描述規則定義這些表(請參閱 NDB$OLD()),則 NDB$EPOCH()NDB$EPOCH_TRANS() 都會將衝突列的項目插入到相關的例外狀況表中。您必須先建立任何例外狀況表,然後再建立要使用它的資料表。

與本節中討論的其他衝突偵測函數一樣,透過在 mysql.ndb_replication 表中包含相關項目來啟動 NDB$EPOCH()NDB$EPOCH_TRANS()(請參閱 ndb_replication 表)。在此案例中,主要和次要 NDB 叢集的作用完全由 mysql.ndb_replication 表項目決定。

由於 NDB$EPOCH()NDB$EPOCH_TRANS() 採用的衝突偵測演算法是不對稱的,因此您必須對主要和次要複本的 server_id 項目使用不同的值。

僅在 DELETE 操作之間發生的衝突不足以使用 NDB$EPOCH()NDB$EPOCH_TRANS() 觸發衝突,並且在週期內的相對位置並不重要。

NDB$EPOCH() 的限制

目前在使用 NDB$EPOCH() 執行衝突偵測時,適用以下限制

  • 衝突偵測是利用 NDB Cluster 的 epoch 邊界進行,其精細度與 TimeBetweenEpochs (預設值:100 毫秒) 成比例。最小衝突視窗是指在兩個叢集上同時更新相同資料時,一定會回報衝突的最短時間。這個時間長度永遠不會是零,且大致與 2 * (延遲 + 排隊 + TimeBetweenEpochs) 成比例。這表示,假設 TimeBetweenEpochs 使用預設值,且忽略叢集之間的任何延遲(以及任何排隊延遲),則最小衝突視窗大小約為 200 毫秒。在考量預期的應用程式競爭模式時,應將此最小視窗納入考量。

  • 使用 NDB$EPOCH()NDB$EPOCH_TRANS() 函數的表格需要額外的儲存空間;每列需要 1 到 32 位元的額外空間,取決於傳遞給函數的值。

  • 刪除操作之間的衝突可能會導致主要和次要叢集之間的差異。當同時在兩個叢集上刪除列時,可以偵測到衝突,但不會記錄,因為該列已被刪除。這表示在傳播任何後續重新對齊操作期間,不會偵測到進一步的衝突,這可能會導致差異。

    刪除操作應在外部序列化,或僅路由至一個叢集。或者,應該以交易方式更新單獨的列,並包含此類刪除和其後的任何插入,以便可以追蹤列刪除之間的衝突。這可能需要變更應用程式。

  • 目前,當使用 NDB$EPOCH()NDB$EPOCH_TRANS() 進行衝突偵測時,僅支援雙向主動-主動設定中的兩個 NDB Cluster。

  • 目前,具有 BLOBTEXT 欄位的表格不支援使用 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() 受相同的條件和限制約束,此外,它還要求使用 --ndb-log-transaction-id 設定為 ON,將所有交易 ID 記錄在次要叢集的二進位日誌中。這會增加一些變動的額外負荷(每列最多 13 個位元組)。

請參閱 NDB$EPOCH()

NDB$EPOCH2()

NDB$EPOCH2() 函數類似於 NDB$EPOCH(),不同之處在於 NDB$EPOCH2() 提供雙向複製拓撲的刪除-刪除處理。在這種情況下,透過在每個來源上將 ndb_conflict_role 系統變數設定為適當的值(通常每個來源各一個 PRIMARYSECONDARY),將主要和次要角色指派給兩個來源。完成此操作後,次要來源所做的修改會由主要來源反映回次要來源,然後次要來源會條件式地套用這些修改。

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_countNdb_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_idsource_server_idsource_epochcount 欄位使用此處顯示的名稱,並對與原始表格主索引鍵欄位相符的欄位使用與原始表格相同的名稱。

如果例外狀況表格使用本節稍後討論的一個或多個選用欄位 NDB$OP_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID,則每個必要的欄位也必須使用前置詞 NDB$ 來命名。如果需要,即使您未定義任何選用欄位,也可以使用 NDB$ 前置詞來命名必要的欄位,但在這種情況下,所有四個必要的欄位都必須使用前置詞來命名。

在這些欄位之後,應依照它們在定義原始表格的主索引鍵時所使用的順序複製組成原始表格主索引鍵的欄位。複製原始表格主索引鍵欄位的欄位資料類型應與原始欄位的資料類型相同(或更大)。可以使用主索引鍵欄位的子集。

例外狀況表格必須使用 NDB 儲存引擎。(本節稍後將示範使用 NDB$OLD() 和例外狀況表格的範例。)

可以在複製的主索引鍵欄位之後選擇性地定義其他欄位,但不能在其之前的任何欄位;任何此類額外欄位都不能為 NOT NULL。NDB Cluster 支援三個額外的預定義選用欄位 NDB$OP_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID,這些欄位將在接下來的幾個段落中說明。

NDB$OP_TYPE:此欄位可用於取得導致衝突的操作類型。如果您使用此欄位,請按此處所示定義它

NDB$OP_TYPE ENUM('WRITE_ROW', 'UPDATE_ROW', 'DELETE_ROW',
    'REFRESH_ROW', 'READ_ROW') NOT NULL

WRITE_ROWUPDATE_ROWDELETE_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_ROWWRITE_ROW 操作的原因回報;ROW_ALREADY_EXISTS 可作為 WRITE_ROW 事件的回報。DATA_IN_CONFLICT 在基於列的衝突函數偵測到衝突時回報;TRANS_IN_CONFLICT 在事務衝突函數拒絕屬於完整事務的所有操作時回報。

NDB$ORIG_TRANSIDNDB$ORIG_TRANSID 欄位(如果使用)包含起始事務的 ID。此欄位應定義如下

NDB$ORIG_TRANSID BIGINT UNSIGNED NOT NULL

NDB$ORIG_TRANSID 是由 NDB 產生的 64 位元值。此值可用於關聯來自相同或不同例外狀況表格的屬於相同衝突事務的多個例外狀況表格項目。

不是原始表格主索引鍵一部分的其他參考欄位可以命名為 colname$OLDcolname$NEWcolname$OLD 參考更新和刪除操作中的舊值,也就是包含 DELETE_ROW 事件的操作。colname$NEW 可用於參考插入和更新操作中的新值,換句話說,使用 WRITE_ROW 事件、UPDATE_ROW 事件或這兩種事件的操作。當衝突操作未提供給定參考欄位(不是主索引鍵)的值時,例外狀況表格列包含 NULL 或該欄位已定義的預設值。

重要

當為複寫設定資料表格時,會讀取 mysql.ndb_replication 表格,因此在建立要複寫的表格之前,必須將對應於要複寫表格的列插入 mysql.ndb_replication 中。

衝突偵測狀態變數

可以使用數個狀態變數來監視衝突偵測。您可以透過 NDB$EPOCH() 查看自上次從 Ndb_conflict_fn_epoch 系統狀態變數的目前值重新啟動此複本以來,找到多少列發生衝突。

Ndb_conflict_fn_epoch_trans 提供直接由 NDB$EPOCH_TRANS() 發現衝突的列數。Ndb_conflict_fn_epoch2Ndb_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 之外,未使用的列主索引鍵會插入例外狀況表格,如本節其他部分所述。

另請參閱 第 25.4.3.9.3 節「NDB Cluster 狀態變數」

範例

以下範例假設您已具備有效的 NDB Cluster 複寫設定,如第 25.7.5 節「準備 NDB Cluster 進行複寫」第 25.7.6 節「啟動 NDB Cluster 複寫 (單一複寫通道)」中所述。

NDB$MAX() 範例。假設您希望在表格 test.t1 上啟用 較大時間戳記獲勝 衝突解決,並使用欄位 mycol 作為 時間戳記。這可以使用以下步驟完成

  1. 請確認您已使用 --ndb-log-update-as-write=OFF 啟動來源 mysqld

  2. 在來源上,執行此 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) 相同;會使用伺服器預設值。

  3. 建立 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;

需要以下步驟,依顯示的順序

  1. 首先,而且在建立 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 欄位。

  2. 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_TYPENDB$CFT_CAUSENDB$ORIG_TRANSID 其中一個欄位,因此四個必要欄位都需要 NDB$ 前綴。

  3. 如先前所示,建立表格 test.t2

對於您希望使用 NDB$OLD() 執行衝突解決的每個表格,都必須遵循這些步驟。對於每個此類表格,mysql.ndb_replication 中都必須有對應的列,而且必須有與複寫表格位於相同資料庫中的例外狀況表格。

讀取衝突偵測與解決。 NDB Cluster 也支援追蹤讀取操作,這使得在循環複寫設置中,可以管理在一個叢集中讀取給定列與在另一個叢集中更新或刪除同一列之間的衝突。這個範例使用 employeedepartment 資料表來模擬一個情境,其中員工從來源叢集(我們在下文中稱之為叢集 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 來規避此問題。以這種方式取得獨佔讀取鎖定,會導致在來源上讀取的任何列被標記為需要在複本叢集上進行衝突解決。如果我們在記錄這些交易之前以此方式啟用獨佔讀取,則會在複本叢集上追蹤讀取,並將其傳送到叢集 A 進行解決;隨後會偵測到 employee 列的衝突,且叢集 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 中的兩個資料表 t1t2,並且希望針對 t1 使用 NDB$MAX_INS(),針對 t2 使用 NDB$MAX_DEL_WIN_INS() 來使用插入衝突偵測。這兩個資料表會在設定程序的稍後才建立。

設定插入衝突解決類似於設定先前範例中顯示的其他衝突偵測和解決演算法。如果用於設定二進位記錄和衝突解決的 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 陳述式,為資料表 t1t2 的複寫衝突偵測建立例外資料表

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 欄位會作為時間戳記欄位使用。

一旦在來源上建立,t1t2 就會複寫,並且可以假設存在於來源和複本上。在此範例的其餘部分,我們使用 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)

在來源上插入的列,其時間戳記大於複本上衝突列中的時間戳記,已取代那些列。在複本上,我們接下來插入兩列不會與 t1t2 中任何現有列衝突的新列,如下所示

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)

如我們先前所見,來源上插入的其他列沒有被複本拒絕,只有時間戳記值小於複本上衝突列中的列才被拒絕。