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