數據異常不用怕,GaiaDB有辦法
背景
在大多數業務場景中,數據庫主要負責提供對當前數據的查詢能力。然而,對于一些特定業務場景,如游戲運營和金融交易等,查詢歷史數據的能力尤為重要。例如,在游戲場景中,通過查詢歷史數據來追蹤和定位賬號裝備的異常變動。在數據恢復、審計、分析等場景,閃回查詢可以幫助用戶了解數據的歷史變化,以便進行決策支持或問題診斷。此外,在日常的運維過程中,如果發生誤操作,閃回查詢提供了更為靈活的時間點選擇。與依賴歷史備份進行日志回放相比,它能夠更快實現數據恢復。
GaiaDB的閃回查詢通過一條簡單的SQL語句,即可訪問歷史數據。無論是需要糾正剛剛發生的錯誤操作,還是進行長期趨勢的數據分析,GaiaDB都能提供快速、準確的數據查詢服務。這不僅極大地減輕了業務流程中對數據查詢的需求復雜度,也提高了數據管理的效率。本文深入探討了GaiaDB閃回查詢技術,并提供了示例演示。
使用方法
GaiaDB引入了新的查詢語法來支持閃回查詢,只需要在select語句后添加AS OF TIMESTAMP + 時間戳
便可以查詢某一歷史版本的數據。語法如下:
SELECT column_name_list FROM table_name AS OF TIMESTAMP time_expr alias WHERE...;
技術方案
1、基于undo log的版本鏈
GaiaDB的閃回查詢功能利用MVCC(多版本并發控制)特性實現。在原生的InnoDB引擎中,MVCC特性的設計目的是解決只讀事務和寫事務之間的沖突。根據數據庫不同的隔離級別的限制,寫事務的修改對于讀事務來說有著不同的可見性。為了實現這樣的可見性需求,InnoDB中每行數據都維護了修改自己的undo日志,這些undo記錄按照從新到舊的順序組成了本行數據的歷史版本鏈,如下圖所示。在查詢時,可以通過這個版本鏈向前追溯歷史版本,通過記錄的事務號和本查詢的ReadView進行可見性判斷,從而查詢出當前讀事務所能讀到的正確數據。出于節省空間的考慮,歷史版本鏈不會無限增長,若某個歷史版本對于所有事務都不再需要,那么便可以將該歷史版本清理掉。
2、GaiaDB對于MVCC讀取的改造
如果讀事務持有某一歷史時刻的ReadView,并且此時刻的歷史版本尚未被清理,那么就能很自然地獲得需要的歷史數據版本。因此閃回查詢的實現,首先是保存歷史的ReadView及對應的數據版本。GaiaDB新增了一個系統表History ReadViews,表中的每行數據為一個二元組,分別是時間戳和該時間戳對應的ReadView經序列化后的字符串,如下圖所示。在開啟閃回查詢之后,后臺線程會以1秒為周期,從事務系統獲取當前的ReadView,經序列化后與當前的時間戳組成二元組,插入到系統表中。在清理歷史版本鏈時,限制條件也發生了變化。History ReadViews表中最早時間戳對應的ReadView決定了undo記錄是否可以被清理。如下圖示例中,系統表中最早的時間戳為"2024-04-04 16:16:16",那么以此時刻的ReadView為標準,已經提交事務所相關的undo記錄才能夠被清理。
在需要查詢某一歷史時刻的版本時,只需從系統表中獲取到該時刻對應的ReadView作為本次查詢的ReadView,即可查詢到特定時刻的歷史數據。以下圖為例,若某行數據在T0時刻值為“A”,在T1時刻被修改為了“B”,又在T2時刻被修改為了“C”。此時進行普通的查詢,會首先在事務系統中獲取當前時刻的ReadView,由于之前的修改都已提交,所以該ReadView顯示能夠讀取到最新版本的數據,然后從B+樹上查詢得到數據值為“C”。
若使用閃回查詢,查詢T0時刻的數據,那么首先會從History ReadViews系統表中查詢T0時刻的ReadView。由于在T0時刻,后續的修改還未發生,所以該ReadView顯示只有最早的歷史版本1對于本次查詢來說是可見的。使用這個ReadView再去數據行的歷史版本鏈上遍歷,首先發現最新版本"C"是不可見的,繼續向前追溯,直至發現可見的歷史版本1,返回結果為“A”。
3、IO暴漲問題和解決方案
在InnoDB引擎中,二級索引不記錄事務號,所以在MVCC中二級索引的可見性判斷,需要回查主鍵判斷。又由于每個undo記錄可能分布在不同的數據頁上,所以每個undo記錄的訪問都可能觸發IO。開啟閃回查詢時,歷史版本的清理被推遲,歷史版本鏈很長,這會導致InnoDB的性能急劇下降,極端情況會導致實例保護性自殺。針對這個問題,GaiaDB將歷史版本清理的過程拆分為兩個階段。第一個階段為二級索引的清理,這個過程在事務提交之后即可進行。經過這一階段,所有對應二級索引上的delete mark被刪除,只保留其在主鍵索引上的位置。第二個階段為主鍵索引的清理,這一階段的進度由系統表History ReadViews中最老的ReadView限制,逐秒推進。經過這一階段,對應的主鍵索引上的delete mark也被清除,完成最終的清理任務。這種方案舍棄了提供二級索引數據閃回查詢的能力,但是很好地解決了IO暴漲的問題。
具體示例
下面用一個具體的使用場景說明如何用GaiaDB輕松地獲取歷史數據。
1、使用如下語句創建表結構并插入初始數據。
create table products (prod_id bigint(10) primary key NOT NULL,prod_name varchar(20) NOT NULL,cust_id bigint(10) NULL,createtime datetime NOT NULL DEFAULT NOW());
INSERT INTO products(prod_id,prod_name,cust_id,createtime) values (101,'Book',1,NOW()),(102,'Apple',1,NOW()),(103,'Beef',2,NOW()),(104,'Bread',3,NOW()),(105,'Cheese',4,NOW());
2、記錄下當前的時間戳,2024-04-12 16:42:41。
SELECT NOW();
3、對數據進行修改,修改prod_name
為Book和Apple的prod_id
,由101和102分別修改為110和119
UPDATE products SET prod_id = 110, createtime = NOW() WHERE prod_name = "Book";
UPDATE products SET prod_id = 119, createtime = NOW() WHERE prod_name = "Apple";
4、使用普通的查詢語句可以查詢到更新后的數據。
SELECT * FROM products;
+---------+-----------+---------+---------------------+
| prod_id | prod_name | cust_id | createtime |
+---------+-----------+---------+---------------------+
| 103 | Beef | 2 | 2024-04-12 16:42:35 |
| 104 | Bread | 3 | 2024-04-12 16:42:35 |
| 105 | Cheese | 4 | 2024-04-12 16:42:35 |
| 110 | Book | 1 | 2024-04-12 16:42:56 |
| 119 | Apple | 1 | 2024-04-12 16:42:57 |
+---------+-----------+---------+---------------------+
5 rows in set (0.00 sec)
5、使用閃回查詢的as of timestamp語法查詢查看products
表中2024-04-12 16:42:41
這個歷史時間點的數據。
SELECT * FROM products AS of TIMESTAMP '2024-04-12 16:42:41';
+---------+-----------+---------+---------------------+
| prod_id | prod_name | cust_id | createtime |
+---------+-----------+---------+---------------------+
| 101 | Book | 1 | 2024-04-12 16:42:35 |
| 102 | Apple | 1 | 2024-04-12 16:42:35 |
| 103 | Beef | 2 | 2024-04-12 16:42:35 |
| 104 | Bread | 3 | 2024-04-12 16:42:35 |
| 105 | Cheese | 4 | 2024-04-12 16:42:35 |
+---------+-----------+---------+---------------------+
可以看到prod_name
為Book和Apple的prod_id
為未修改前的101和102。