國產數據庫與Oracle數據庫事務差異分析
數據庫中的ACID是事務的基本特性,而在Oracle等數據庫遷移到國產數據庫國產中,可能因為不同數據庫事務處理機制的不同,在遷移后的業務邏輯處理上存在差異。本文簡要介紹了事務的ACID屬性、事務的隔離級別、回滾機制和超時機制,并總結SAVEPOINT的使用,以總結。
1、數據庫中事務基本概念
事務是數據庫中的基本邏輯操作單元,由一組不可分割的數據庫操作序列組成,這些操作要么全部成功執行,要么全部失敗回滾。其核心目的是確保數據的完整性和一致性,尤其在并發操作或系統故障時維護數據庫的可靠狀態。
1.1 事務基本屬性
ACID是事務的基本特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability)。
- 原子性:事務中的所有操作必須作為一個整體執行,要么全部執行成功、要么全部失敗回滾,不允許出現部分成功的情況。在數據庫中通常是通過日志記錄(如undo log)來實現回滾操作,若事務執行失敗,系統跟進日志撤銷已執行的操作。
- 一致性:事務執行前后,數據庫必須保持一致性狀態。所有數據必須滿足預定義的完整性約束(如主鍵、外鍵、唯一性約束等)。即使事務失敗,數據庫也不能破壞這些規則。在數據庫中通過一些約束和檢查來確保數據庫的完整性約束。
- 隔離性:多個事務并發執行時,每個事務的操作應與其他事務相互隔離,使得每個事務感覺不到其他事務的存在,最終效果應與事務串行執行的結果一致。數據庫中通過鎖機制(Locking)或多版本并發控制(MVCC)實現,不同的隔離級別提供不同程度的隔離性。
- 持久性:事務一旦提交,其對數據庫的修改就是永久性的,即使系統發生故障(如斷電、崩潰),修改也不會丟失。數據庫中通過重做日志(Redo Log)實現持久性。提交事務時,對數據的修改首先寫入日志,再異步寫入數據庫文件中。當數據庫崩潰恢復時,通過重放日志恢復數據。
以轉賬交易為例,通過undo日志實現原子性,確保“扣款”和“存款”兩個操作要么全部成功,要么全部失敗;一致性是確保轉賬前后,數據庫必須滿足業務規則(如余額不為負、總額不變);通過鎖機制和MVCC多版本并發控制來實現事務的隔離性,多個并發轉賬操作互不干擾,結果與串行執行一致;持久性則是一旦轉賬成功,即使系統崩潰,修改也不會丟失。
BEGIN TRANSACTION;
-- 1. 檢查一致性:用戶A余額是否足夠(一致性)
SELECT balance FROM accounts WHERE user = 'A' FOR UPDATE;
-- 如果余額 < 100,拋出錯誤并回滾
-- 2. 扣款(原子性)
UPDATE accounts SET balance = balance - 100 WHERE user = 'A';
-- 3. 存款(原子性)
UPDATE accounts SET balance = balance + 100 WHERE user = 'B';
-- 4. 提交(持久性)
COMMIT;
1.2 事務隔離級別
事務隔離級別是數據庫事務處理的基礎,SQL-92標準定義了4種隔離級別:讀未提交(READ UNCOMMITTED)、讀已提交(READ COMMITTED)、可重復讀(REPEATABLE READ)、串行化(SERIALIZABLE)。詳見下表:
不同的隔離級別有不同的現象,并有不同的鎖和并發機制。隔離級別越高,數據庫的并發性能就越差。
1.2.1 臟讀/不可重復讀/幻讀現象
1)臟讀A事務讀取B事務尚未提交的更改數據,并在這個數據的基礎上進行操作,這時候如果事務B回滾,那么A事務讀到的數據是不被承認的。
2)不可重復讀不可重復讀是指在同一個事務中,同一個查詢在T1時刻讀取一行數據,在T2時刻重新讀取這一行數據的時候,發現這一行數據已經發生了修改(被更新或者刪除)。假如A在取款事務的過程中,B往該賬戶轉賬100,A兩次讀取的余額發生不一致。
3)幻讀幻讀是指在同一個事務中,當同一個查詢多次執行的時候,由于其它插入操作的事務提交,會導致每次返回不同的結果集。不可重復讀和幻讀的區別是:前者是指讀到了已經提交的事務的更改數據(修改或刪除),后者是指讀到了其他已經提交事務的新增數據。
1.2.2 行鎖模式
- Share:lock owner和任何并發程序可以read但是不能change locked page或row,并發程序可能獲得S-lock、U-lock,也可能沒有lock就進行讀操作
- Update:lock owner可read但是不能change locked page或row,但是owner可以將U-lock升級為X-lock這樣就可以修改page或row
- 升級為X-lock這個過程可能會引起其它S-lock的并發進程暫停在那
- 當lock owner讀數據的時候并決定是否需要修改它的時候,U-lock會減少deadlocks的幾率
- Exclusive:只有lock owner才能read或change locked page或row,并發程序只有當程序處于UNCOMMITTED read isolation的時候才能訪問數據
- Lock mode compatibility,見下表
圖片
比如說User A對page hold住S-lock,如果User B想對page請求X-lock,則User A的lockmode會拒絕User B的請求。
1.2.3 隔離級別
1)讀未提交(Read Uncommitted)讀未提交,就是一個事務可以讀取另一個未提交事務的數據,也稱為臟讀。在讀數據時候不加鎖,寫數據時候加行級別的共享鎖,提交時釋放鎖。行級別的共享鎖,不會對讀產生影響,但是可以防止兩個同時的寫操作
2)讀已提交(Read Committed)讀提交,就是一個事務要等另一個事務提交后才能讀取它的數據,否則是讀取不到另外一個事務的更改的數據。事務讀取數據(讀到數據的時候)加行級共享S鎖,讀完釋放;事務寫數據時候(寫操作發生的瞬間)加行級獨占X鎖,事務結束釋放。由于事務寫操作加上獨占X鎖,因此事務寫操作時,讀操作也不能進行,因此,不能讀到事務的未提交數據,避免了臟讀的問題。但是由于,讀操作的鎖加在讀上面,而不是加在事務之上,所以,在同一事務的兩次讀操作之間可以插入其他事務的寫操作,所以可能發生不可重復讀的問題。
3)可重復讀(Repeatable Read)當事務隔離級別為可重復讀時,只能讀到該事務啟動時已經提交的其他事務修改的數據,未提交的數據或在事務啟動后其他事務提交的數據是不可見的。對于本事務而言,事務語句可以看到之前的語句做出的修改。事務讀取數據在讀操作開始的瞬間就加上行級共享S鎖,而且在事務結束的時候才釋放。但是,由于加的是行級別的鎖,仍然可能發生幻讀的問題。
4)序列化(Serialization)最嚴格的隔離級別,強制事務串行執行,使之不可能沖突,從而解決幻讀的問題,資源消耗最大。在讀操作時,加表級共享鎖,事務結束時釋放;寫操作時候,加表級獨占鎖,事務結束時釋放。在這個級別,可能會導致大量的鎖超時和鎖競爭現象,實際上也很少用到。
1.2.4 不同數據庫隔離級別
不同數據庫支持的隔離級別也不同
圖片
- Oracle數據庫支持讀已提交和序列化,默認隔離級別為Read Committed,通過通過多版本并發控制(MVCC)避免臟讀,但存在不可重復讀和幻讀。
- MySQL數據庫支持四種隔離級別,默認為可重復讀,通過MVCC和間隙鎖來減少幻讀問題。
- MySQL數據庫支持四種隔離級別,默認為Read Committed
- OceanBase(for Oracle)模式支持讀已提交和序列化,默認為Read Committed
- TiDB支持讀已提交、可重復讀和序列化,默認為Repeatable Read,其實在TiDB中實現是快照隔離,類似可重復讀;
- GoldenDB兼容MySQL的隔離機制,支持4種隔離級別,但是默認的級別是Read Committed,也是并發和一致性平衡的結果;
- GaussDB支持Read Committed和Repeatable Read,默認隔離級別是Read Committed
1.3 事務回滾機制
事務的原子性要求事務要么全部執行成功、要么全部執行失敗回滾,但是對于Oracle數據庫支持語句級的原子性,也就是一個事務中單個語句執行失敗,則只會回滾該語句執行的操作,不會導致在當前事務中丟失之前的任何工作。如果需要回滾整個事務,需要處理錯誤并且主動調用ROLLBACK。這種語句級的回滾對于處理一些長時間運行的批處理任務有用,邏輯上希望能夠處理錯誤,不需要回滾已經完成的所有操作。
圖片
不過從Oracle數據庫遷移到國產數據庫中,大部分數據庫在事務的回滾機制上并不支持語句級別的,因此需要通過采用SAVEPOINT保存點的方式。使用SAVEPOINT雖然可以解決語句級別功能上的需求,但是不合理的使用可能引發其它問題。
1.4 事務超時機制
數據庫中事務會設置不同的超時機制,防止因為出現等鎖而出現無限等待,超過這個時間后會出現等鎖超時,事務會回滾。
圖片
- Oracle數據庫:默認不會主動終止因行鎖等待而阻塞的事務,事務會無限期等待鎖釋放,需由應用層處理或手動終止,行鎖在事務提交或回滾是自動釋放;事務默認也無超時設置,但是可以限制會話的空閑時間,超過時間后會斷開鏈接。
- MySQL數據庫:通過 innodb_lock_wait_timeout 控制,默認為50秒。當事務等待鎖超過此時間時,會拋出錯誤;事務中無默認超時時間,但是連接的空閑超時設置wait_timeout,默認8小時。
- PostgreSQL:通過pg_lock_timeout設置行鎖等待超時時間,默認為0無限等待;事務中設置statement_timeout 控制單條SQL執行時間,默認無限制。
- TiDB:兼容MySQL行鎖等待設置;如果是悲觀事務,默認TTL(Time-To-Live)為 1小時,超時后自動回滾,另外通過tidb_idle_transaction_timeout 控制空閑事務。
- OceanBase:MySQL模式下兼容MySQL設置;事務中通過 ob_query_timeout 控制事務單條語句執行時間,默認1800s
- GoldenDB:兼容MySQL設置
- GaussDB:參數lockwait_timeout控制單個鎖的最長等待時間,當申請的鎖等待時間超過設定值時,系統會報錯,默認為20min;通過通過 statement_timeout 控制單個語句執行時長,默認0表示不控制。
1.5 如何使用SAVEPOINT
前文提到Oracle數據庫中支持語句級別的回滾,在遷移到國產數據庫后,為了兼容Oracle數據庫這個特性,很多數據庫支持SAVEPOINT機制。SAVEPOINT是事務中的一個邏輯標記點,用于標識事務執行到某個特定位置的狀態。通過ROLLBACK TO SAVEPOINT可以回滾到該標記點,撤銷該點之后的所有操作,但保留該點之前的操作。SAVEPOINT可以提供細粒度的事務控制,避免因整個事務回滾導致的數據丟失。對于一些復雜或長時間運行的事務中,可以分階段提交或回滾操作。
BEGIN
INSERT INTO orders (id, amount) VALUES (1, 100);
SAVEPOINT sp1; -- 設置保存點sp1
UPDATE inventory SET stock = stock - 1 WHERE product_id = 101;
SAVEPOINT sp2; -- 設置保存點sp2
-- 假設此處發生錯誤
ROLLBACK TO sp1; -- 回滾到sp1,撤銷UPDATE操作
COMMIT; -- 提交事務(僅保留INSERT操作)
END;
但是在一個長事務中不規范的使用SAVEPOINT可能會導致實例內存上漲、事務執行時間異常的問題。比如在Oracle數據庫中的一個游標查詢語句,每1K筆執行一次,遷移到GaussDB之后,URL串中指定了autosave參數,這樣在每次游標訪問時候都會執行一次SAVEPOINT動作,原來幾分鐘的任務可能幾個小時都執行不完,出現很多SAVEPOINT等待事件,并且數據庫實例的內存逐漸上漲。
那么對于一些復雜的業務場景,如何合理的使用SAVEPOINT?
- 禁止自動設置SAVEPOINT,只在必要時設置SAVEPOINT,如在數據更新、復雜邏輯分支操作上設置;
- 將長事務拆分為多個小事務,每處理一定的數據后提交,以釋放資源;
- 避免在游標循環中頻繁執行SAVEPOINT,改用批量處理或分頁查詢;
- 及時回滾或釋放無用SAVEPOINT,在邏輯分支完成后,主動回滾到最近的SAVEPOINT并釋放資源;
- 避免嵌套過深的SAVEPOINT:過多的嵌套SAVEPOINT會增加回滾段的復雜度。