聊聊數據庫中的 savepoint

從全局二級索引講起

故事要從全局二級索引開始講起。 當我們構建了一個全局二級索引之後,一條邏輯上的數據插入,就會變成兩條物理上的數據插入:一條插入到主表,另一條插入到索引表。爲了保證主表和索引表數據的一致性,我們往往需要開啓分佈式事務,再並行地插入兩條數據。如果其中一條數據插入失敗了,比如索引上出現了唯一鍵衝突,但主表的數據已經插了進去,怎麼辦呢?當然,我們可以簡單粗暴地回滾整個事務,來保證數據的一致性。 但有的時候,我們已經在事務裏執行了大量的操作,這時候僅僅因爲一條數據的插入失敗,就要回滾整個事務,代價實在太大。對於單機 MySQL 來說,如果出現了這種插入 UK 報唯一鍵衝突的情況,會自動回滾這條插入的語句。至於是忽略報錯繼續執行事務,還是回滾整個事務,則交給業務方來決定。作爲一款全面兼容 MySQL 的分佈式數據庫,PolarDB-X 自然也要具備這種特性。 其實,不只是全局二級索引的情況,其他場景比如 batch insert/delete/update、廣播表 DML 等都可能會遇到這種情況。

聊聊 savepoint

如果要回滾單條或多條語句,而非回滾整個事務,我們自然想到使用 savepoint 這一功能。在事務中,我們可以隨時設置一個 savepoint,後續再回滾到這個 savepoint,從而回滾 savepoint 後的所有操作。 MySQL 是如何實現 savepoint 能力的呢? MySQL 在 server 層中,對每個事務對象維護了一個 savepoint 的鏈表,用於記錄這個事務設置過的 savepoint 對象。其中,每個 savepoint 對象主要記錄了 savepoint 的名字,用於標識不同的 savepoint 對象。 在設置一個 savepoint 時,會往鏈表末尾插入一個 savepoint 對象。在釋放一個 savepoint 時,會根據 savepoint 名字遍歷鏈表,找到對應的 savepoint 對象,將其及其後面的所有 savepoint 刪除。在回滾一個 savepoint 時,會找到對應的 savepoint 對象,根據其存儲的信息進行回滾操作,隨後,還會隱式釋放掉其後的所有 savepoint(不包括它自己)。 可以看到,每個 savepoint 對象都需要存儲一定的信息,來告訴 binlog 和 innodb 需要回滾到什麼位置。對於 binlog 記錄的是設置 savepoint 時的 binlog cache 的 offset;對於 innodb,則是設置 savepoint 時 undo log 的 undo number。這兩個簡單的信息,就足夠 binlog 和 innodb 完成回滾操作了。 事實上,innodb 內部還維護了事務的 savepoint 鏈表,但本質上和上述說的鏈表沒什麼太大差異,就不展開討論了。

使用 savepoint 解決問題

那 PolarDB-X 該如何使用 DN 的 savepoint 解決一開始提到的全局二級索引的問題呢? 其實做法也很簡單,我們只需要在任何物理語句執行之前,加上一個 savepoint,在所有物理語句執行之後,視情況來回滾或是釋放 savepoint。我們將這一行爲稱爲 auto-savepoint。 其實,innodb 的行爲也是如此,其在每條語句前(實際是上一條語句執行後),會更新一個匿名的 savepoint 對象 last_sql_stat_start,其保存了上一條語句執行後的 undo number。在當前語句執行出錯時,通過這個 undo number 來回滾掉這條語句的操作。 熟悉 PolarDB-X 的同學一定知道,PolarDB-X 通過物理連接(計算節點到存儲節點的連接)來執行物理 SQL。對於一條邏輯更新 GSI 的 SQL 語句,可能需要使用 2 條物理連接,執行 3 條物理 SQL(一條主表 update,一條 GSI 表刪除,一條 GSI 表插入)。如下所示:

物理連接 0(物理分庫 0): 
update primary_tb; insert gsi_tb; 
物理連接 1(物理分庫 1): 
delete gsi_tb;

設置 auto-savepoint 的關鍵就在於要在合適的時機設置 savepoint。在這個例子中,任何一個物理連接執行出錯,都會通知其他連接中斷其正在執行的操作。假設在物理連接 1 執行 delete gsi_tb 的時候報錯了,我們不知道物理連接 0 上的具體執行情況。哪些語句執行成功了、哪些語句執行失敗了、哪些語句還沒開始執行,我們都不知道。此時,我們可以藉助 savepoint 的能力,不管具體的執行情況如何,都統一回滾到一切操作還沒開始做的狀態,就能達到回滾單條邏輯 SQL 的效果。 因此,我們自動設置的 savepoint 行爲就是:

物理連接 0(物理分庫 0): 
savepoint `s0`; update primary_tb; insert gsi_tb; rollback to savepoint `s0`;
物理連接 1(物理分庫 1): 
savepoint `s0`; delete gsi_tb (ERROR); rollback to savepoint `s0`;

當然,這裏面的設計還會保證參與了一條邏輯 SQL 的所有物理連接都正確設置上 savepoint,以保證 savepoint 的設置和回滾都不會漏掉,否則就會出現數據不一致的問題了。

代價是什麼

我們通過 DN 的 savepoint 能力,來實現 CN 層面上的回滾單條語句的功能。儘管從前面的討論來看,設置和釋放 savepoint 的代價都比較低,只是在鏈表上新增或刪除一個元素,但我們還是需要在實現上儘量減輕這種代價。 首先,我們儘量避免 savepoint 的設置,只在涉及 GSI 或其他邏輯執行的 DML 時,才自動設置 savepoint。因爲只有在邏輯執行下,纔可能發生分片間不一致的場景,才需要 auto-savepoint 來保證邏輯語句的原子性。其次,我們設置和釋放都是通過多語句的方式,將 savepoint 的 SQL 和業務產生的物理 SQL 一併下發,避免增加額外的 RTT。最後,我們還使用了私有協議繞過 savepoint SQL 的解析過程,直接在 DN 上調用設置和釋放 savepoint 的代碼。

作者:勿遮

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章