數據庫事務隔離級別怎么選?看完這篇你就懂了!
一、背景介紹
事務隔離,是每場高級開發面試過程中,必不可少的一個重點環節。在上篇文章中,我們介紹了 Spring 的事務管理實踐,相信大家對它有了一個初步的認識,今天我們就一起來捋一捋數據庫的事務隔離機制。
說到事務,相信你一定不會陌生,在與數據庫打交道的時候,我們總是會用到它。
以轉賬為例,你要給朋友小張轉賬 100 元,而此時的銀行卡余額至少要有 100元。在轉賬過程中,程序會有一系列的操作,比如查詢余額、做加減法、更新余額等,這些操作必須保證是一體的。
不然等程序查完之后,還沒做扣減余額之前,你這 100 塊錢,完全可以借著這個時間差再查一次,然后再給另外一個朋友轉賬,如果程序真的這么搞,銀行不血虧才怪!
在整個程序更新數據過程中,這時就要用到“事務”這個概念了。
簡單的說,事務就是要保證一組數據庫操作,要么全部成功,要么全部失敗!
以 MySQL 為例,事務支持是在引擎層實現的,可能你知道,MySQL 是一個支持多引擎的系統,但并不是所有的引擎都支持事務,比如 MySQL 原生的 MyISAM 引擎就不支持事務,這也是 MyISAM 被 InnoDB 取代的重要原因之一。
下面將會以 InnoDB 為例,剖析 MySQL 在事務支持方面的特定實現,希望通過這些案例能加深你對 MySQL 事務原理的理解!
二、事務隔離機制介紹
提到事務,大家會不由自主的想到 ACID (Atomicity、Consistency、Isolation、Durability)四大特性,即:原子性、一致性、隔離性、持久性。
原子性、一致性很好理解,就是上文說道的,要么全部成功,要么全部失敗;持久性,也好理解,當數據發生變化時,能將最新的結果記錄到磁盤中永久保存;而隔離性,有點復雜,簡單的說,就是將事務彼此之間隔離開,當多個事務在同時處理一個數據時,彼此之間互相不影響。
如果隔離的不夠好,就有可能會產生臟讀、不可重復度、幻讀等讀現象。
為此,隔離性總共分為四種級別:由低到高依次為 Read uncommitted 、Read committed 、Repeatable read 、Serializable ,這四個級別可以逐個解決臟讀 、不可重復讀 、幻讀等這幾類問題。
- read uncommitted:俗稱讀未提交,指的是一個事務還沒提交時,它做的變更就能被別的事務看到。
- Read committed:俗稱讀提交,指的是一個事務提交之后,它做的變更才會被其他事務看到。
- Repeatable read:俗稱可重復讀,指的是一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一致的,同時當其他事務在未提交時,變更是不可見的。
- Serializable:俗稱串行化,顧名思義就是對于同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突的時候,后訪問的事務必須等前一個事務執行完成,才能繼續執行。
在這四個隔離級別中,其中“讀提交”和“可重復讀”比較難理解,下面我們以一個例子為案例,介紹這幾種隔離級別的區別!
假設數據表 T 中只有一列,其中一行的值為 1。
mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);
下面是按照時間順序執行兩個事務的行為。
圖片
我們來看看在不同的隔離級別下,事務 A 會有哪些不同的返回結果,也就是圖里面 V1、V2、V3 的返回值分別是什么。
- 若隔離級別是“讀未提交”, 則 V1 的值就是 2,這時候事務 B 雖然還沒有提交,但是結果已經被 A 看到了,因此,V1、V2、V3 也都是 2。
- 若隔離級別是“讀提交”,則 V1 是 1,事務 B 的更新在提交后才能被 A 看到。所以, V2、V3 的值也是 2。
- 若隔離級別是“可重復讀”,則 V1、V2 是 1,V3 是 2。之所以 V2 還是 1,遵循的就是這個要求:當前事務在執行期間看到的數據前后必須是一致的。
- 若隔離級別是“串行化”,則在事務 B 執行“將 1 改成 2”的時候,會被鎖住。直到事務 A 提交后,事務 B 才可以繼續執行。所以從 A 的角度看, V1、V2 值是 1,V3 的值是 2。
為什么會產生這種情況呢,下面我們一起來分析一下!實現上,當開啟事務時,數據庫里面會創建一個視圖,訪問的時候以視圖的邏輯結果為準。
- 在“讀提交”隔離級別下,這個視圖是在每個 SQL 語句開始執行的時候創建的。
- 在“可重復讀”隔離級別下,這個視圖是在事務啟動時創建的,整個事務存在期間都用這個視圖。
- 在“串行化”隔離級別下,直接用加鎖的方式來避免并行訪問。
- 而在“讀未提交”隔離級別下,直接返回記錄上的最新值,沒有視圖概念。
因此,在不同的隔離級別下,數據庫行為是有所不同的,比如 Mysql 數據庫的默認隔離級別就是“可重復讀”,而 oracle、pgsql 數據庫的默認隔離級別是“讀提交”。
因此對于一些從 Oracle 遷移到 MySQL 的應用,為保證數據庫隔離級別的一致,你一定要記得將 MySQL 的隔離級別設置為“讀提交”。
配置的方式是,將啟動參數transaction-isolation的值設置成READ-COMMITTED。
你可以用show variables來查看當前的事務隔離級別。
mysql> show variables like '%tx_isolation%';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| tx_isolation | REPEATABLE-READ |
+-----------------------+----------------+
通過如下方式,可以將其進行重新設置,比如設置為“讀提交”
mysql> set tx_isolatinotallow='READ-COMMITTED';
總結來說,每種隔離級別都有自己的使用場景,你要根據自己的業務情況來定!
可能有的同學會發出疑問,“讀提交”就可以解決問題,為什么還要搞個“可重復讀”隔離機制呢?
下面我們以數據校對的邏輯為案例,來介紹一下“可重復讀”的隔離好處!
假設你在管理一個個人銀行賬戶表,一個表存了賬戶余額,一個表存了賬單明細,到了月底你要做數據校對,也就是判斷上個月的余額和當前余額的差額,是否與本月的賬單明細一致。你一定希望在數據校對過程中,即使有用戶發生了一筆新的交易,也不影響你的校對結果。
這時候使用“可重復讀”的隔離級別就極其實用而又方便,因為事務啟動時的視圖可以認為是靜態的,不受其他事務更新的影響。
三、如何理解臟讀、不可重復讀、幻讀?
在上面我們介紹了隔離機制,當出現多個事務同時處理一條數據的時候,就會產生一些問題,具體來說就是:臟讀、不可重復讀和幻讀。
3.1、臟讀
臟讀指的是讀到了其他事務未提交的數據,未提交意味著這些數據可能會保存到數據庫,也可能會回滾,不保存到數據庫。當這個數據發生了回滾,就意味著這個數據不存在,這就是臟讀!
以上面的案例為例,當隔離級別為“讀未提交”時,V1 的值就是 2,假如事務 B 最后沒有提交數據,相當于讀取了一條不存在的數據,這就會產生臟讀,一旦產生臟讀會很嚴重,會整個業務影響很大。
3.2、不可重復讀
不可重復讀指的是在一個事務內,最開始讀到的數據和事務結束前的任意時刻讀到的同一批數據出現不一致的情況。
以上面的案例為例,當隔離級別為“讀提交”時,就會產生同一個事務,多次讀取同一條數據會產生不同的結果。
3.3、幻讀
幻讀和不可重復讀,有點類似,同一個事務多次讀同一條數據結果不一致,但是表達的側重點不一樣。
比如,當事務 A 在查詢某條記錄是否存在,如果不存在就插入,在準備插入時,突然事務 B 也提交一條插入語句,而且提交速度快于事務 A,這個時候事務 A 在插入數據的時候,突然報錯,插入不了,此時就發生了幻讀!
不可重復讀側重表達:讀-讀,幻讀則側重表達:讀-寫,用寫來證實讀的是鬼影。
圖片
上述所說的"臟讀","不可重復讀","幻讀"這些問題,其實本質都是因為并發操作造成的從數據庫讀數據不一致的問題。
首先說讀未提交,它是性能最好,也可以說它是最野蠻的方式,因為它壓根兒就不加鎖,所以根本談不上什么隔離效果,可以理解為沒有隔離。
再來說串行化,串行化就相當于上面所說的,處理一個人請求的時候,別的人都等著。并發效率最差。
最后說讀提交和可重復讀。這兩種隔離級別都是比較復雜的,既要允許一定的并發,又想要兼顧解決問題。
數據庫的事務隔離越嚴格,并發副作用越小,但付出的代價越大;因為事務隔離本質就是使事務在一定程度上處于串行狀態,這本身就是和并發相矛盾的。
不同的應用對讀一致性和事務隔離級別是不一樣的,比如許多應用對數據的一致性沒那么高要求,相反,對并發有一定要求,具體的隔離機制的設置還需要從實際的業務需求和系統情況出發。
對于幻讀這種問題,可以在數據插入或者更新的時候,通過增加樂觀鎖來解決數據寫入失敗問題。
四、事務隔離的實現
理解了事務的隔離級別,我們再來看看事務隔離具體是怎么實現的。這里我們展開說明“可重復讀”。
在 MySQL 中,實際上每條記錄在更新的時候,都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。
假設一個值從 1 被按順序改成了 2、3、4,在回滾日志里面就會有類似下面的記錄。
圖片
當前值是 4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的 read-view。
如圖中看到的,在視圖 A、B、C 里面,這一個記錄的值分別是 1、2、4,同一條記錄在系統中可以存在多個版本,就是數據庫的多版本并發控制(MVCC)。對于 read-view A,要得到 1,就必須將當前值依次執行圖中所有的回滾操作得到。
同時你會發現,即使現在有另外一個事務正在將 4 改成 5,這個事務跟 read-view A、B、C 對應的事務是不會沖突的。
你一定會問,回滾日志總不能一直保留吧,什么時候刪除呢?
答案是,在不需要的時候才刪除。也就是說,系統會判斷,當沒有事務再需要用到這些回滾日志時,回滾日志會被刪除。
什么時候才不需要了呢?就是當系統里沒有比這個回滾日志更早的 read-view 的時候。
基于上面的說明,我們來討論一下為什么建議盡量不要使用長事務。
長事務意味著系統里面會存在很老的事務視圖。
由于這些事務隨時可能訪問數據庫里面的任何數據,所以這個事務提交之前,數據庫里面它可能用到的回滾記錄都必須保留,這就會導致大量占用存儲空間。
在 MySQL 5.5 及以前的版本,回滾日志是跟數據字典一起放在 ibdata 文件里的,即使長事務最終提交,回滾段被清理,文件也不會變小。我見過數據只有 20GB,而回滾段有 200GB 的庫。最終只好為了清理回滾段,重建整個庫。
除了對回滾段的影響,長事務還占用鎖資源,也可能拖垮整個庫。
五、小結
本篇主要介紹了事務隔離相關理論知識,以及圍繞 MySQL 的事務隔離實現介紹,如果有描述不對的地方,歡迎指正!
六、參考
1、https://blog.csdn.net/weixin_38019299/article/details/121266965
2、https://baijiahao.baidu.com/s?id=1717095300761675602&wfr=spider&for=pc