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