事务的基本概念,Mysql事务处理原理
本文大綱:
MYSQL事務(wù)大綱初識(shí)事務(wù)
為什么需要事務(wù)?
這里又要掏出那個(gè)爛大街的銀行轉(zhuǎn)賬案例了,以A、B兩個(gè)賬戶(hù)的轉(zhuǎn)賬為例,假設(shè)現(xiàn)在要從A賬戶(hù)向B賬戶(hù)中轉(zhuǎn)入1000員,當(dāng)進(jìn)行轉(zhuǎn)賬時(shí),需要先從銀行賬戶(hù)A中取出錢(qián),然后再存入銀行賬戶(hù)B中,SQL樣本如下:
//?第一步:A賬戶(hù)余額減少減少1000?? update?balance?set?money?=?money?-500?where?name=?‘A’; //?第二步:B賬戶(hù)余額增加1000?? update?balance?set?money?=?money?+500?where?name=?‘B’;如果在完成了第1步的時(shí)候突然宕機(jī)了,A的錢(qián)減少了而B(niǎo)的錢(qián)沒(méi)有增加,那A豈不是白白丟了1000元,這時(shí)候就需要用到我們的事務(wù)了,開(kāi)啟事務(wù)后SQL樣本如下:
//?第一步:開(kāi)始事務(wù) start?transaction; //?第二步:A賬戶(hù)余額減少減少1000?? update?balance?set?money?=?money?-500?where?name=?‘A’; //?第三步:B賬戶(hù)余額增加1000?? update?balance?set?money?=?money?+500?where?name=?‘B’; //?第四步:提交事務(wù) commit;什么是事務(wù)
事務(wù)(Transaction)是訪(fǎng)問(wèn)和更新數(shù)據(jù)庫(kù)的程序執(zhí)行單元;事務(wù)中可能包含一個(gè)或多個(gè)sql語(yǔ)句,這些語(yǔ)句要么都執(zhí)行成功,要么全部執(zhí)行失敗。
事務(wù)的四大特性(ACID)
原子性(Atomicity,或稱(chēng)不可分割性)
「一個(gè)事務(wù)必須被視為一個(gè)不可分割的最小工作單元,整個(gè)事務(wù)中所有的操作要么全部提交成功,要么全部失敗回滾,對(duì)于一個(gè)事務(wù)來(lái)說(shuō),不可能只執(zhí)行其中的一部分操作,這就是事務(wù)的原子性」
一致性(Consistency)
「數(shù)據(jù)庫(kù)總是從一個(gè)一致性的狀態(tài)轉(zhuǎn)換到另外一個(gè)一致性的狀態(tài),在事務(wù)開(kāi)始之前和之后,數(shù)據(jù)庫(kù)的完整性約束沒(méi)有被破壞。在前面的例子中,事務(wù)結(jié)束前后A、B賬戶(hù)總額始終保持不變」
隔離性(Isolation)
「隔離性是指,事務(wù)內(nèi)部的操作與其他事務(wù)是隔離的,并發(fā)執(zhí)行的各個(gè)事務(wù)之間不能互相干擾。嚴(yán)格的隔離性,對(duì)應(yīng)了事務(wù)隔離級(jí)別中的Serializable (可串行化),但實(shí)際應(yīng)用中出于性能方面的考慮很少會(huì)使用可串行化。」
持久性(Durability)
「持久性是指事務(wù)一旦提交,它對(duì)數(shù)據(jù)庫(kù)的改變就應(yīng)該是永久性的。接下來(lái)的其他操作或故障不應(yīng)該對(duì)其有任何影響。」
事務(wù)的隔離級(jí)別
在前文中我們介紹了隔離性,但實(shí)際上隔離性比想象的要復(fù)雜的多。在SQL標(biāo)準(zhǔn)中定義了四種隔離級(jí)別,每一種隔離級(jí)別都規(guī)定了一個(gè)事務(wù)所做的修改,哪些在事務(wù)內(nèi)和事務(wù)間是可見(jiàn)的,哪些是不可見(jiàn)的,較低級(jí)別的隔離通常可以執(zhí)行跟高的并發(fā),系統(tǒng)的開(kāi)銷(xiāo)也更低
未提交讀(READ UNCOMMITTED)
在這個(gè)隔離級(jí)別下,事務(wù)的修改即使沒(méi)有提交,對(duì)其他事務(wù)也是可見(jiàn)的。事務(wù)可以讀取未提交的數(shù)據(jù),這也被稱(chēng)之為臟讀。這個(gè)級(jí)別會(huì)帶來(lái)很多問(wèn)題,從性能上來(lái)說(shuō),READ UNCOMMITTED不會(huì)比其他的級(jí)別好太多,但是卻會(huì)帶來(lái)很多問(wèn)題,除非真的有非常必要的理由,在實(shí)際應(yīng)用中一般很少使用。
提交讀(REDA COMMITED)
大多數(shù)數(shù)據(jù)系統(tǒng)的默認(rèn)隔離級(jí)別都是REDA COMMITED(MySql不是),REDA COMMITED滿(mǎn)足前面提到的隔離性的簡(jiǎn)單定義:一個(gè)事務(wù)開(kāi)始時(shí),只能看到已經(jīng)提交的事務(wù)所做的修改。換句話(huà)說(shuō),一個(gè)事物從開(kāi)始直到提交前,所做的修改對(duì)其他事務(wù)不可見(jiàn)。這個(gè)級(jí)別有時(shí)候也叫做不可重復(fù)讀,因?yàn)閳?zhí)行兩次相同的查詢(xún)可能會(huì)得到不同的結(jié)果。
可重復(fù)讀(REPEATABLE READ)
REPEATABLE READ解決了臟讀以及不可重復(fù)度的問(wèn)題。該級(jí)別保證了同一個(gè)事務(wù)多次讀取同樣記錄的結(jié)果是一致的。但是理論上,可重復(fù)度還是無(wú)法解決另外一個(gè)幻讀的問(wèn)題。所謂幻讀,指的是當(dāng)某個(gè)事務(wù)在讀取某個(gè)范圍內(nèi)的記錄時(shí),另外一個(gè)事務(wù)又在該范圍內(nèi)插入了新的記錄,當(dāng)之前的事務(wù)再次讀取該范圍的記錄時(shí),就會(huì)產(chǎn)生幻行。
不可重復(fù)讀跟幻讀的區(qū)別在于,「前者是數(shù)據(jù)發(fā)生了變化,后者是數(shù)據(jù)的行數(shù)發(fā)生了變化」。
可串行化(SERIALIZABLE)
SERIALIZABLE是最高的隔離級(jí)別,它通過(guò)強(qiáng)制事務(wù)串行執(zhí)行,避免前面說(shuō)的幻讀。簡(jiǎn)單來(lái)說(shuō)SERIALIZABLE會(huì)在讀取的每一行數(shù)據(jù)上都加鎖,所以可能會(huì)導(dǎo)致大量的超時(shí)和鎖爭(zhēng)用的問(wèn)題。實(shí)際應(yīng)用中也很少使用這個(gè)隔離級(jí)別,只有在非常需要確保數(shù)據(jù)一致性而且可以接受沒(méi)有并發(fā)的情況下,才考慮此級(jí)別。
保存點(diǎn)
我們可以在事務(wù)執(zhí)行的過(guò)程中定義保存點(diǎn),在回滾時(shí)直接指定回滾到指定的保存點(diǎn)而不是事務(wù)開(kāi)始之初,有點(diǎn)像我們玩游戲的時(shí)候可以存檔而不是每次都要重新再來(lái)
定義保存點(diǎn)的語(yǔ)法如下:
SAVEPOINT?保存點(diǎn)名稱(chēng);當(dāng)我們想回滾到某個(gè)保存點(diǎn)時(shí),可以使用下邊這個(gè)語(yǔ)句(下邊語(yǔ)句中的單詞WORK和SAVEPOINT是可有可無(wú)的):
ROLLBACK?[WORK]?TO?[SAVEPOINT]?保存點(diǎn)名稱(chēng);MySQL中的事務(wù)跟原理
MySQL中的事務(wù)
「MySQL中不是所有的存儲(chǔ)引擎都支持事務(wù)」,例如MyISAM就不支持事務(wù),實(shí)際上支持事務(wù)的只有InnoDB跟NDB Cluster,「本文關(guān)于事務(wù)的分析都是基于InnoDB」
「MySQL默認(rèn)采用的是自動(dòng)提交的方式」,也就是說(shuō)如果不是顯示的開(kāi)始一個(gè)事務(wù),則系統(tǒng)會(huì)自動(dòng)向數(shù)據(jù)庫(kù)提交結(jié)果。在當(dāng)前連接中,還可以通過(guò)設(shè)置AUTOCONNIT變量來(lái)啟用或者禁用自動(dòng)提交模式。
開(kāi)啟自動(dòng)提交功能
MySQL中默認(rèn)情況下的自動(dòng)提交功能是已經(jīng)開(kāi)啟的。
關(guān)閉自動(dòng)提交功能。
關(guān)閉自動(dòng)提交功能后,只用當(dāng)執(zhí)行COMMIT命令后,MySQL才將數(shù)據(jù)表中的資料提交到數(shù)據(jù)庫(kù)中。如果執(zhí)行ROLLBACK命令,數(shù)據(jù)將會(huì)被回滾。如果不提交事務(wù),而終止MySQL會(huì)話(huà),數(shù)據(jù)庫(kù)將會(huì)自動(dòng)執(zhí)行回滾操作。
「MySQL的默認(rèn)隔離級(jí)別是可重復(fù)讀(REPEATABLE READ)」。
事務(wù)的實(shí)現(xiàn)原理
我們要探究MySQL中事務(wù)的實(shí)現(xiàn)原理,實(shí)際上就是要弄明天它的ACID特性是如何實(shí)現(xiàn)的,在這里有必要先說(shuō)明的是,「ACID中的一致性是事務(wù)的最終目標(biāo),前面提到的原子性、持久性和隔離性,都是為了保證數(shù)據(jù)庫(kù)狀態(tài)的一致性」。所以我們要分析的就是MySQL的原子性、持久性和隔離性的實(shí)現(xiàn)原理,在分析事務(wù)的實(shí)現(xiàn)原理之前我們需要補(bǔ)充一些InnoDB的相關(guān)知識(shí)
InnoDB是一個(gè)將表中的數(shù)據(jù)存儲(chǔ)到磁盤(pán)上的存儲(chǔ)引擎,所以即使關(guān)機(jī)后重啟我們的數(shù)據(jù)還是存在的。而真正「處理數(shù)據(jù)的過(guò)程是發(fā)生在內(nèi)存中的」,「所以需要把磁盤(pán)中的數(shù)據(jù)加載到內(nèi)存中,如果是處理寫(xiě)入或修改請(qǐng)求的話(huà),還需要把內(nèi)存中的內(nèi)容刷新到磁盤(pán)上」。而我們知道讀寫(xiě)磁盤(pán)的速度非常慢,和內(nèi)存讀寫(xiě)差了幾個(gè)數(shù)量級(jí),所以當(dāng)我們想從表中獲取某些記錄時(shí),InnoDB存儲(chǔ)引擎需要一條一條的把記錄從磁盤(pán)上讀出來(lái)么?不,那樣會(huì)慢死,InnoDB采取的方式是:「將數(shù)據(jù)劃分為若干個(gè)頁(yè),以頁(yè)作為磁盤(pán)和內(nèi)存之間交互的基本單位,InnoDB中頁(yè)的大小一般為 16 KB。也就是在一般情況下,一次最少?gòu)拇疟P(pán)中讀取16KB的內(nèi)容到內(nèi)存中,一次最少把內(nèi)存中的16KB內(nèi)容刷新到磁盤(pán)中。」
我們還需要對(duì)MySQL中的日志有一定了解。MySQL的日志有很多種,如二進(jìn)制日志(bin log)、錯(cuò)誤日志、查詢(xún)?nèi)罩尽⒙樵?xún)?nèi)罩镜?#xff0c;此外InnoDB存儲(chǔ)引擎還提供了兩種事務(wù)日志:「redo log(重做日志)和undo log(回滾日志)。其中redo log用于保證事務(wù)持久性;undo log則是事務(wù)原子性和隔離性實(shí)現(xiàn)的基礎(chǔ)。」
InnoDB作為MySQL的存儲(chǔ)引擎,數(shù)據(jù)是存放在磁盤(pán)中的,但如果每次讀寫(xiě)數(shù)據(jù)都需要磁盤(pán)IO,效率會(huì)很低。為此,InnoDB提供了「緩存(Buffer Pool)」,Buffer Pool中包含了磁盤(pán)中部分?jǐn)?shù)據(jù)頁(yè)的映射,作為訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)的緩沖:「當(dāng)從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)時(shí),會(huì)首先從Buffer Pool中讀取,如果Buffer Pool中沒(méi)有,則從磁盤(pán)讀取后放入Buffer Pool;當(dāng)向數(shù)據(jù)庫(kù)寫(xiě)入數(shù)據(jù)時(shí),會(huì)首先寫(xiě)入Buffer Pool,Buffer Pool中修改的數(shù)據(jù)會(huì)定期刷新到磁盤(pán)中(這一過(guò)程稱(chēng)為刷臟)。」
InnoDB存儲(chǔ)引擎文件主要可以分為兩類(lèi),表空間文件及重做日志文件(redo log file),表空間文件又可以細(xì)分為兩類(lèi),共享表空間跟獨(dú)立表空間。「undo log位于共享表空間中的undo段中」,每個(gè)表空間都被劃分成了若干個(gè)頁(yè)面,「凡是頁(yè)面的讀寫(xiě)都在buffer pool中進(jìn)行,這意味著undo log也需要先寫(xiě)入到buffer pool,所以u(píng)ndo log的生成也需要持久化,也就是說(shuō)undo log的生成需要記錄對(duì)應(yīng)的redo log」。(注意:不是所有的undo log的生成都會(huì)產(chǎn)生對(duì)應(yīng)的redo log,對(duì)于操作臨時(shí)表生成的undo log并不會(huì)生成對(duì)應(yīng)的undo log,因?yàn)樾薷呐R時(shí)表而產(chǎn)生的undo日志只需要在系統(tǒng)運(yùn)行過(guò)程中有效,如果系統(tǒng)奔潰了,那么在重啟時(shí)也不需要恢復(fù)這些undo日志所在的頁(yè)面,所以在寫(xiě)針對(duì)臨時(shí)表的Undo頁(yè)面時(shí),并不需要記錄相應(yīng)的redo日志。)
持久性實(shí)現(xiàn)原理
通過(guò)前面的補(bǔ)充知識(shí)我們知道InnoDB引入了Buffer Pool來(lái)優(yōu)化讀寫(xiě)的性能,但是雖然Buffer Pool優(yōu)化了性能,但同時(shí)也帶來(lái)了新的問(wèn)題:「如果MySQL宕機(jī),而此時(shí)Buffer Pool中修改的數(shù)據(jù)還沒(méi)有刷新到磁盤(pán),就會(huì)導(dǎo)致數(shù)據(jù)的丟失,事務(wù)的持久性無(wú)法保證」。
基于此,redo log就誕生了,「redo log是物理日志,記錄的是數(shù)據(jù)庫(kù)中數(shù)據(jù)庫(kù)中物理頁(yè)的情況」,redo log包括兩部分:一是內(nèi)存中的日志緩沖(redo log buffer),該部分日志是易失性的;二是磁盤(pán)上的重做日志文件(redo log file),該部分日志是持久的。在概念上,innodb通過(guò)「force log at commit」機(jī)制實(shí)現(xiàn)事務(wù)的持久性,即在事務(wù)提交的時(shí)候,必須先將該事務(wù)的所有事務(wù)日志寫(xiě)入到磁盤(pán)上的redo log file和undo log file中進(jìn)行持久化。
看到這里可能有的小伙伴又會(huì)有疑問(wèn)了,既然redo log也需要在事務(wù)提交時(shí)將日志寫(xiě)入磁盤(pán),為什么它比直接將Buffer Pool中修改的數(shù)據(jù)寫(xiě)入磁盤(pán)(即刷臟)要快呢?主要有以下兩方面的原因:
(1)刷臟是隨機(jī)IO,因?yàn)槊看涡薷牡臄?shù)據(jù)位置隨機(jī),但寫(xiě)redo log是追加操作,屬于順序IO。
(2)刷臟是以數(shù)據(jù)頁(yè)(Page)為單位的,MySQL默認(rèn)頁(yè)大小是16KB,一個(gè)Page上一個(gè)小修改都要整頁(yè)寫(xiě)入;而redo log中只包含真正需要寫(xiě)入的部分,無(wú)效IO大大減少。
這里我以文章開(kāi)頭的例子進(jìn)行說(shuō)明redo log為何能保證持久性:
//?第一步:開(kāi)始事務(wù) start?transaction; //?第二步:A賬戶(hù)余額減少減少1000?? update?balance?set?money?=?money?-500?where?name=?‘A’; //?第三步:B賬戶(hù)余額增加1000?? update?balance?set?money?=?money?+500?where?name=?‘B’; //?第四步:提交事務(wù) commit; redo?這里需要對(duì)redo log的刷盤(pán)補(bǔ)充一點(diǎn)內(nèi)容:
MySQL支持用戶(hù)自定義在commit時(shí)如何將log buffer中的日志刷log file中。這種控制通過(guò)變量 innodb_flush_log_at_trx_commit 的值來(lái)決定。該變量有3種值:0、1、2,「默認(rèn)為1」。但注意,這個(gè)變量只是控制commit動(dòng)作是否刷新log buffer到磁盤(pán)。
當(dāng)設(shè)置為1的時(shí)候,事務(wù)每次提交都會(huì)將log buffer中的日志寫(xiě)入os buffer并調(diào)用fsync()函數(shù)刷到log file on disk中。這種方式即使系統(tǒng)崩潰也不會(huì)丟失任何數(shù)據(jù),但是因?yàn)槊看翁峤欢紝?xiě)入磁盤(pán),IO的性能較差。
當(dāng)設(shè)置為0的時(shí)候,事務(wù)提交時(shí)不會(huì)將log buffer中日志寫(xiě)入到os buffer(內(nèi)核緩沖區(qū)),而是每秒寫(xiě)入os buffer并調(diào)用fsync()寫(xiě)入到log file on disk中。也就是說(shuō)設(shè)置為0時(shí)是(大約)每秒刷新寫(xiě)入到磁盤(pán)中的,當(dāng)系統(tǒng)崩潰,會(huì)丟失1秒鐘的數(shù)據(jù)。
當(dāng)設(shè)置為2的時(shí)候,每次提交都僅寫(xiě)入到os buffer,然后是每秒調(diào)用fsync()將os buffer中的日志寫(xiě)入到log file on disk。
「可以看到設(shè)置為0或者2時(shí),都有可能丟失1s的數(shù)據(jù)」
?
原子性實(shí)現(xiàn)原理
前面提到了,所謂原子性就是指整個(gè)事務(wù)是一個(gè)不可分隔的整體,組成事務(wù)的一組SQL要么全部成功,要么全部失敗,要達(dá)到這個(gè)目的就意味著當(dāng)某一個(gè)SQL執(zhí)行失敗時(shí),我們要能夠撤銷(xiāo)掉其它SQL的執(zhí)行結(jié)果,在MySQL中這是依賴(lài)undo log(回滾日志)來(lái)實(shí)現(xiàn)。
undo log屬于「邏輯日志」(前面提到的redo log屬于物理日志,記錄的是數(shù)據(jù)頁(yè)的情況),我們可以這么認(rèn)為,「當(dāng)delete一條記錄時(shí),undo log中會(huì)記錄一條對(duì)應(yīng)的insert記錄,反之亦然,當(dāng)update一條記錄時(shí),它記錄一條對(duì)應(yīng)相反的update記錄。」
但執(zhí)行發(fā)生異常時(shí),會(huì)根據(jù)undo log中的記錄進(jìn)行回滾。undo log主要分為兩種
insert undo log
update undo log
「insert undo log是指在insert 操作中產(chǎn)生的undo log」,因?yàn)閕nsert操作的記錄,只對(duì)事務(wù)本身可見(jiàn),對(duì)其他事務(wù)不可見(jiàn)。故該undo log可以在事務(wù)提交后直接刪除,不需要進(jìn)行purge操作。
「而update undo log記錄的是對(duì)delete 和update操作產(chǎn)生的undo log」,該undo log可能需要提供MVCC機(jī)制,因此不能再事務(wù)提交時(shí)就進(jìn)行刪除。提交時(shí)放入undo log鏈表,等待purge線(xiàn)程進(jìn)行最后的刪除。
?補(bǔ)充:purge線(xiàn)程兩個(gè)主要作用是:清理undo頁(yè)和清除page里面帶有Delete_Bit標(biāo)識(shí)的數(shù)據(jù)行。在InnoDB中,事務(wù)中的Delete操作實(shí)際上并不是真正的刪除掉數(shù)據(jù)行,而是一種Delete Mark操作,在記錄上標(biāo)識(shí)Delete_Bit,而不刪除記錄。是一種"假刪除",只是做了個(gè)標(biāo)記,真正的刪除工作需要后臺(tái)purge線(xiàn)程去完成。
?
這里我們就來(lái)看看insert undo log的結(jié)構(gòu),如下:
insert undo在上圖中,undo type記錄的是undo log的類(lèi)型,對(duì)于insert undo log,該值始終為11(TRX_UNDO_INSERT_REC),undo no在一個(gè)事務(wù)中是從0開(kāi)始遞增的,也就是說(shuō)只要事務(wù)沒(méi)提交,每生成一條undo日志,那么該條日志的undo no就增1。table id記錄undo log所對(duì)應(yīng)的表對(duì)象。如果記錄中的主鍵只包含一個(gè)列,那么在類(lèi)型為T(mén)RX_UNDO_INSERT_REC的undo日志中只需要把該列占用的存儲(chǔ)空間大小和真實(shí)值記錄下來(lái),如果記錄中的主鍵包含多個(gè)列(復(fù)合主鍵),那么每個(gè)列占用的存儲(chǔ)空間大小和對(duì)應(yīng)的真實(shí)值都需要記錄下來(lái)(圖中的len就代表列占用的存儲(chǔ)空間大小,value就代表列的真實(shí)值),「在回滾時(shí)只需要根據(jù)主鍵找到對(duì)應(yīng)的列然后刪除即可」。end of record記錄了下一條undo log在頁(yè)面中開(kāi)始的地址,start of record記錄了本條undo log在頁(yè)面中開(kāi)始的地址。
對(duì)undo log有一定了解后,我們?cè)倩仡^看看文章開(kāi)頭的例子,分析下為什么undo log能保證原子性
//?第一步:開(kāi)始事務(wù) start?transaction; //?第二步:A賬戶(hù)余額減少減少1000?? update?balance?set?money?=?money?-500?where?name=?‘A’; //?第三步:B賬戶(hù)余額增加1000?? update?balance?set?money?=?money?+500?where?name=?‘B’; //?第四步:提交事務(wù) commit; undo redo考慮到排版,這里我只畫(huà)了一條語(yǔ)句的流程圖,第二條也是一樣的,每次更新或者插入前,先記錄undo,再修改內(nèi)存中數(shù)據(jù),再記錄redo。
隔離性實(shí)現(xiàn)原理
我們知道,一個(gè)事務(wù)中的讀操作是不會(huì)影響到另外一個(gè)事務(wù)的,所以在討論隔離性我們主要分為兩種情況
一個(gè)事務(wù)中的寫(xiě)操作,對(duì)另外一個(gè)事務(wù)中寫(xiě)操作的影響
一個(gè)事務(wù)中的寫(xiě)操作,對(duì)另外一個(gè)事務(wù)中讀操作的影響
寫(xiě)操作之間的隔離是通過(guò)鎖來(lái)實(shí)現(xiàn)的,MySQL中的鎖機(jī)制要詳細(xì)來(lái)講是很復(fù)雜的,要講明白整個(gè)鎖需要從索引開(kāi)始介紹,限于筆者能力及文章篇幅,本文只對(duì)MySQL中的鎖機(jī)制做一個(gè)簡(jiǎn)單的介紹
MySQL中的鎖機(jī)制(InnoDB)
讀鎖跟寫(xiě)鎖
讀鎖又稱(chēng)為共享鎖`,簡(jiǎn)稱(chēng)S鎖,顧名思義,共享鎖就是多個(gè)事務(wù)對(duì)于同一數(shù)據(jù)可以共享一把鎖,「都能訪(fǎng)問(wèn)到數(shù)據(jù),但是只能讀不能修改。」
寫(xiě)鎖又稱(chēng)為排他鎖`,簡(jiǎn)稱(chēng)X鎖,顧名思義,排他鎖就是不能與其他所并存,如一個(gè)事務(wù)獲取了一個(gè)數(shù)據(jù)行的排他鎖,其他事務(wù)就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務(wù)是可以對(duì)數(shù)據(jù)就行讀取和修改。
行鎖跟表鎖
表鎖在操作數(shù)據(jù)時(shí)會(huì)鎖定整張表,并發(fā)性能較差;
行鎖則只鎖定需要操作的數(shù)據(jù),并發(fā)性能好。
但是由于加鎖本身需要消耗資源(獲得鎖、檢查鎖、釋放鎖等都需要消耗資源),因此在鎖定數(shù)據(jù)較多情況下使用表鎖可以節(jié)省大量資源。MySQL中不同的存儲(chǔ)引擎支持的鎖是不一樣的,例如MyIsam只支持表鎖,而InnoDB同時(shí)支持表鎖和行鎖,且出于性能考慮,絕大多數(shù)情況下使用的都是行鎖。
意向鎖
意向鎖分為兩種,意向讀鎖(IS)跟意向?qū)戞i(IX)
意向鎖是表級(jí)別的鎖
為什么需要意向鎖呢?思考一個(gè)問(wèn)題:如果我們想對(duì)某個(gè)表加一個(gè)表鎖,那么在加鎖之前我們需要去檢查表中的每一行記錄是否已經(jīng)被單獨(dú)加了行鎖,這樣的話(huà)豈不是意味著我們需要去遍歷表中所有的記錄依次進(jìn)行檢查,遍歷是不可能的,這輩子都不可能遍歷的,基于效率的考慮,我們可以在每次給行記錄加鎖時(shí)先給當(dāng)前表加一個(gè)意向鎖,如果我們要對(duì)行加讀鎖(S)的話(huà),那么就先給表加一個(gè)意向讀鎖(IS),如果要對(duì)行加寫(xiě)鎖(X)的話(huà),那么先給表加一個(gè)意向?qū)戞i(IX),這樣當(dāng)我們需要給整個(gè)表加鎖的時(shí)候就可以通過(guò)先判斷表上是否已經(jīng)存在了意向鎖來(lái)決定是否可以上鎖了,避免遍歷,提高了效率。
意向鎖跟普通的讀鎖寫(xiě)鎖間的兼容性如下:
| IS | IX | S | X | |
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
注:IS(意向讀鎖/意向共享鎖), ?IX(意向?qū)戞i/意向排他鎖), ?S(讀鎖/共享鎖),X(寫(xiě)鎖/排他鎖)
從上圖中可以看出,意向鎖之間都是兼容的,這是因?yàn)橐庀蜴i的作用僅僅是來(lái)快速判斷是否可以直接上表鎖。
「接下來(lái)介紹的這幾種鎖都屬于行鎖」,為了更好的理解這幾種鎖,我們先創(chuàng)建一個(gè)表
CREATE?TABLE?`user`?(`id`?int(11)?NOT?NULL?AUTO_INCREMENT,`name`?varchar(10)?NOT?NULL,PRIMARY?KEY?(`id`), )?ENGINE=InnoDB?AUTO_INCREMENT=7?DEFAULT?CHARSET=utf8;其中id為主鍵,沒(méi)有建其余的索引,插入如下數(shù)據(jù)
INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(1,?'a張大膽'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(3,?'b王翠花'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(6,?'c范統(tǒng)'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(8,?'d朱逸群'); INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(15,?'e董格求');Record Lock(記錄鎖)
鎖定單條記錄
也分為S鎖跟X鎖
如果我們對(duì)id為3的記錄添加一個(gè)行鎖,對(duì)應(yīng)如下(圖中每一列代表數(shù)據(jù)庫(kù)中的一行記錄):
行鎖Gap Lock(間隙鎖)
鎖定一個(gè)范圍,但是不包含記錄本身
間隙鎖的主要作用在于防止幻讀的發(fā)生,雖然也有S鎖跟X鎖的區(qū)分,但是它們的作用都是相同的,而且如果你對(duì)一條記錄加了間隙鎖(不論是共享間隙鎖還是獨(dú)占間隙鎖),并不會(huì)限制其他事務(wù)對(duì)這條記錄加記錄鎖或者繼續(xù)加間隙鎖,再?gòu)?qiáng)調(diào)一遍,間隙鎖的作用僅僅是為了防止幻讀的發(fā)生。
假設(shè)我們要對(duì)id為6的記錄添加間隙鎖,那么此時(shí)鎖定的區(qū)域如下所示
其中虛線(xiàn)框代表的是要鎖定的間隙,其實(shí)就是當(dāng)前需要加間隙鎖的記錄跟上一條記錄之間的范圍,但是間隙鎖不會(huì)鎖定當(dāng)前記錄,如圖所示,id=6的記錄并沒(méi)有被加鎖。(圖中虛線(xiàn)框表鎖間隙,沒(méi)有插入真實(shí)的記錄)
間隙鎖Next-Key Lock(Gap Lock+Record Lock)
假設(shè)我們要對(duì)id為6的記錄添加Next-Key Lock,那么此時(shí)鎖定的區(qū)域如下所示
next key lock跟間隙鎖最大的區(qū)別在于,Next-Key Lock除了鎖定間隙之外還要鎖定當(dāng)前記錄
?通過(guò)鎖實(shí)現(xiàn)了寫(xiě)、寫(xiě)操作之間的隔離性,實(shí)際上我們也可以通過(guò)加鎖來(lái)實(shí)現(xiàn)讀、寫(xiě)之間的隔離性,但是這樣帶來(lái)一個(gè)問(wèn)題,讀、寫(xiě)需要串行執(zhí)行這樣會(huì)大大降低效率,所以MySQL中實(shí)現(xiàn)讀寫(xiě)之間的隔離性是通過(guò)MVCC+鎖來(lái)實(shí)現(xiàn)的,對(duì)于讀采用快照都,對(duì)于寫(xiě)使用加鎖!
?
MVCC(多版本并發(fā)控制)
版本鏈
在介紹MVCC之前我們需要對(duì)MySQL中的行記錄格式有一定了解,其實(shí)除了我們?cè)跀?shù)據(jù)庫(kù)中定義的列之外,每一行中還包含了幾個(gè)隱藏列,分別是
row_id:行記錄的唯一標(biāo)志
transaction_id:事務(wù)ID
roll_pointer:回滾指針
「row_id是行記錄的唯一標(biāo)志,這一列不是必須的。」
MySQL會(huì)優(yōu)先使用用戶(hù)自定義主鍵作為主鍵,如果用戶(hù)沒(méi)有定義主鍵,則選取一個(gè)Unique鍵作為主鍵,如果表中連Unique鍵都沒(méi)有定義的話(huà),則InnoDB會(huì)為表默認(rèn)添加一個(gè)名為row_id的隱藏列作為主鍵。也就是說(shuō)只有在表中既沒(méi)有定義主鍵,也沒(méi)有申明唯一索引的情況MySQL才會(huì)添加這個(gè)隱藏列。
「transaction_id代表的是事務(wù)的ID」。當(dāng)一個(gè)事務(wù)對(duì)某個(gè)表執(zhí)行了增、刪、改操作,那么InnoDB存儲(chǔ)引擎就會(huì)給它分配一個(gè)獨(dú)一無(wú)二的事務(wù)id,分配方式如下:
對(duì)于只讀事務(wù)來(lái)說(shuō),只有在它第一次對(duì)某個(gè)用戶(hù)創(chuàng)建的「臨時(shí)表執(zhí)行增、刪、改操作」時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù)id,否則的話(huà)是不分配事務(wù)id的。
對(duì)于讀寫(xiě)事務(wù)來(lái)說(shuō),只有在它「第一次對(duì)某個(gè)表(包括用戶(hù)創(chuàng)建的臨時(shí)表)執(zhí)行增、刪、改操作」時(shí)才會(huì)為這個(gè)事務(wù)分配一個(gè)事務(wù)id,否則的話(huà)也是不分配事務(wù)id的。
有的時(shí)候雖然我們開(kāi)啟了一個(gè)讀寫(xiě)事務(wù),但是在這個(gè)事務(wù)中全是查詢(xún)語(yǔ)句,并沒(méi)有執(zhí)行增、刪、改的語(yǔ)句,那也就意味著這個(gè)事務(wù)并不會(huì)被分配一個(gè)事務(wù)id。
「roll_pointer表示回滾指針,指向該記錄對(duì)應(yīng)的undo log」。前文已經(jīng)提到過(guò)了,undo log記錄了對(duì)應(yīng)記錄在修改前的狀態(tài),通過(guò)roll_pointer我們就可以找到對(duì)應(yīng)的undo log,然后根據(jù)undo log進(jìn)行回滾。
?在之前介紹undo log的時(shí)候我們只介紹了insert undo log的數(shù)據(jù)格式,實(shí)際上除了insert undo log還有update undo log,而update undo log中也包含roll_pointer跟transaction_id。update undo log中的roll_pointer指針其實(shí)就是保存的被更新的記錄中的roll_pointer指針
?
「除了這些隱藏列以外,實(shí)際上每條記錄的記錄頭信息中還會(huì)存儲(chǔ)一個(gè)標(biāo)志位,標(biāo)志該記錄是否刪除。」
我們以實(shí)際的例子來(lái)說(shuō)明上面三個(gè)隱藏列的作用,還是以之前的表為例,現(xiàn)在對(duì)其執(zhí)行如下SQL:
#?開(kāi)啟事務(wù) START?TRANSACTION; #?插入一條數(shù)據(jù) INSERT?INTO?`test`.`user`(`id`,?`name`)?VALUES?(16,?'e杜子騰'); #?更新插入的數(shù)據(jù) UPDATE?`test`.`user`?SET?name?=?"史珍香"?WHERE?id?=?16; #?刪除數(shù)據(jù) DELETE?from??`test`.`user`?WHERE?id?=?16;我們通過(guò)畫(huà)圖來(lái)看看上面這段SQL在執(zhí)行的過(guò)程中都做了什么
SQL執(zhí)行流程圖從上圖中我們可以看到,每對(duì)記錄進(jìn)行一次增、刪、改時(shí),都會(huì)生成一條對(duì)應(yīng)的undo log,并且被修改后的記錄中的roll pointer指針指向了這條undo log,同時(shí)如果不是新增操作,那么生成的undo log中也會(huì)保存一個(gè)roll pointer,其值是從被修改的數(shù)據(jù)中復(fù)制過(guò)來(lái)了,在我們上邊的例子中update undo log的roll pointer就復(fù)制了insert進(jìn)去的數(shù)據(jù)中的roll pointer指針的值。
另外我們會(huì)發(fā)現(xiàn),根據(jù)當(dāng)前記錄中的roll pointer指針,我們可以找到一個(gè)有undo log組成的鏈表,這個(gè)undo log鏈表其實(shí)就是這條記錄的版本鏈。
ReadView(快照)
對(duì)于使用READ UNCOMMITTED隔離級(jí)別的事務(wù)來(lái)說(shuō),由于可以讀到未提交事務(wù)修改過(guò)的記錄,所以直接讀取記錄的最新版本就好了;
對(duì)于使用SERIALIZABLE隔離級(jí)別的事務(wù)來(lái)說(shuō),MySQL規(guī)定使用加鎖的方式來(lái)訪(fǎng)問(wèn)記錄;
對(duì)于使用READ COMMITTED和REPEATABLE READ隔離級(jí)別的事務(wù)來(lái)說(shuō),都必須保證讀到已經(jīng)提交了的事務(wù)修改過(guò)的記錄,也就是說(shuō)假如另一個(gè)事務(wù)已經(jīng)修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問(wèn)題就是:「需要判斷一下版本鏈中的哪個(gè)版本是當(dāng)前事務(wù)可見(jiàn)的」。
為了解決這個(gè)問(wèn)題,MySQL提出了一個(gè)ReadView(快照)的概念,「在Select操作前會(huì)為當(dāng)前事務(wù)生成一個(gè)快照,然后根據(jù)快照中記錄的信息來(lái)判斷當(dāng)前記錄是否對(duì)事務(wù)是可見(jiàn)的,如果不可見(jiàn)那么沿著版本鏈繼續(xù)往上找,直至找到一個(gè)可見(jiàn)的記錄。」
「ReadView」(快照)中包含了下面幾個(gè)關(guān)鍵屬性:
m_ids:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫(xiě)事務(wù)的事務(wù)id列表。
min_trx_id:表示在生成ReadView時(shí)當(dāng)前系統(tǒng)中活躍的讀寫(xiě)事務(wù)中最小的事務(wù)id,也就是m_ids中的最小值。
max_trx_id:表示生成ReadView時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的id值。
?小貼士:注意max_trx_id并不是m_ids中的最大值,事務(wù)id是遞增分配的。比方說(shuō)現(xiàn)在有id為1,2,3這三個(gè)事務(wù),之后id為3的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成ReadView時(shí),m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
?
creator_trx_id:表示生成該ReadView的事務(wù)的事務(wù)id。
?小貼士:我們前邊說(shuō)過(guò),只有在對(duì)表中的記錄做改動(dòng)時(shí)(執(zhí)行INSERT、DELETE、UPDATE這些語(yǔ)句時(shí))才會(huì)為事務(wù)分配事務(wù)id,否則在一個(gè)只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。
?
當(dāng)生成快照后,會(huì)通過(guò)下面這個(gè)流程來(lái)判斷該記錄對(duì)當(dāng)前事務(wù)是否可見(jiàn)
MVCC從上圖中我們可以看到,在根據(jù)當(dāng)前數(shù)據(jù)庫(kù)中運(yùn)行中的讀寫(xiě)事務(wù)id,會(huì)去生成一個(gè)ReadView。
然后根據(jù)要讀取的數(shù)據(jù)記錄中的事務(wù)id(方便區(qū)別,記為r_trx_id)跟ReadView中保存的幾個(gè)屬性做如下判斷
如果被訪(fǎng)問(wèn)版本的r_trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當(dāng)前事務(wù)在訪(fǎng)問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪(fǎng)問(wèn)。
如果被訪(fǎng)問(wèn)版本的r_trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪(fǎng)問(wèn)。
如果被訪(fǎng)問(wèn)版本的r_trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開(kāi)啟,所以該版本不可以被當(dāng)前事務(wù)訪(fǎng)問(wèn)。
如果被訪(fǎng)問(wèn)版本的r_trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下r_trx_id屬性值是不是在m_ids列表中,如果在,說(shuō)明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪(fǎng)問(wèn);如果不在,說(shuō)明創(chuàng)建ReadView時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪(fǎng)問(wèn)。
如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見(jiàn)的話(huà),那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見(jiàn)性,依此類(lèi)推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見(jiàn)的話(huà),那么就意味著該條記錄對(duì)該事務(wù)完全不可見(jiàn),查詢(xún)結(jié)果就不包含該記錄。
實(shí)際上,提交讀跟可重復(fù)讀在實(shí)現(xiàn)上最大的差異就在于
提交讀每次select都會(huì)生成一個(gè)快照
可重復(fù)讀只有在第一次會(huì)生成一個(gè)快照
總結(jié)
本文主要介紹了事務(wù)的基本概念跟MySQL中事務(wù)的實(shí)現(xiàn)原理。下篇文章開(kāi)始我們就要真正的進(jìn)入Spring的事務(wù)學(xué)習(xí)啦!鋪墊了這么久,終于開(kāi)始主菜了......
在前面的大綱里也能看到,會(huì)分為上下兩篇,第一篇講應(yīng)用以及在使用過(guò)程中會(huì)碰到的問(wèn)題,第二篇我們就深入源碼分析Spring中的事務(wù)機(jī)制的實(shí)現(xiàn)原理!
「參考」:
書(shū)籍:掘金小冊(cè)《MySQL 是怎樣運(yùn)行的:從根兒上理解 MySQL》:https://juejin.im/book/6844733769996304392
書(shū)籍:《MySQL技術(shù)內(nèi)幕:InnoDB存儲(chǔ)引擎》:關(guān)注公眾號(hào),程序員DMZ,后臺(tái)回復(fù)InnoDB即可領(lǐng)取
書(shū)籍:《高性能MySQL》:關(guān)注公眾號(hào),程序員DMZ,后臺(tái)回復(fù)MySQL即可領(lǐng)取
文章:《深入學(xué)習(xí)MySQL事務(wù):ACID特性的實(shí)現(xiàn)原理》:https://www.cnblogs.com/kismetv/p/10331633.html
文章:《詳細(xì)分析MySQL事務(wù)日志(redo log和undo log)》:https://www.cnblogs.com/f-ck-need-u/p/9010872.html
文章:《Mysql事務(wù)實(shí)現(xiàn)原理》:https://www.lagou.com/lgeduarticle/82740.html
文章:《面試官:你說(shuō)熟悉MySQL事務(wù),那來(lái)談?wù)勈聞?wù)的實(shí)現(xiàn)原理吧!》:https://mp.weixin.qq.com/s/jrfZr3YzE_E0l3KjWAz1aQ
文章:《InnoDB 事務(wù)分析-Undo Log》:http://leviathan.vip/2019/02/14/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-Undo-Log/
文章:《InnoDB 的 Redo Log 分析》:http://leviathan.vip/2018/12/15/InnoDB%E7%9A%84Redo-Log%E5%88%86%E6%9E%90/
文章:《MySQL redo & undo log-這一篇就夠了》:https://www.jianshu.com/p/336e4995b9b8
有道無(wú)術(shù),術(shù)可成;有術(shù)無(wú)道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的事务的基本概念,Mysql事务处理原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 20175204 张湲祯 2018-20
- 下一篇: shiro的简单入门使用