深入探究MySQL事務(wù)實(shí)現(xiàn)原理
什么是數(shù)據(jù)庫(kù)事務(wù)
數(shù)據(jù)庫(kù)事務(wù)是指一組數(shù)據(jù)庫(kù)操作,這些操作必須被視為一個(gè)不可分割的單元,要么全部執(zhí)行成功,要么全部失敗回滾。事務(wù)通常由多個(gè)SQL語(yǔ)句組成,這些語(yǔ)句可以讀取、插入、更新或刪除數(shù)據(jù)庫(kù)中的數(shù)據(jù)。
事務(wù)具有ACID屬性:
- 原子性(Atomicity):事務(wù)的所有操作被視為單個(gè)原子操作,要么全部執(zhí)行成功,要么全部執(zhí)行失敗回滾。
- 一致性(Consistency):事務(wù)執(zhí)行的結(jié)果必須使數(shù)據(jù)庫(kù)從一個(gè)一致性狀態(tài)轉(zhuǎn)換到另一個(gè)一致性狀態(tài),其中包括所有數(shù)據(jù)完整性和約束性規(guī)則的應(yīng)用。
- 隔離性(Isolation):一個(gè)事務(wù)的執(zhí)行不能被其他并發(fā)執(zhí)行的事務(wù)干擾,每個(gè)事務(wù)應(yīng)該感覺(jué)自己在獨(dú)立地執(zhí)行。
- 持久性(Durability):一旦事務(wù)提交,其結(jié)果應(yīng)該持久保存在數(shù)據(jù)庫(kù)中,即使系統(tǒng)故障也應(yīng)該如此。
通過(guò)實(shí)現(xiàn)事務(wù),數(shù)據(jù)庫(kù)系統(tǒng)可以確保數(shù)據(jù)的完整性和一致性,以及并發(fā)訪問(wèn)時(shí)的正確性。如果一個(gè)事務(wù)中的任何一個(gè)操作失敗,整個(gè)事務(wù)將被回滾到最初的狀態(tài),這確保了數(shù)據(jù)庫(kù)的一致性。
Mysql如何保證原子性
undo log名為回滾日志,是實(shí)現(xiàn)原子性的關(guān)鍵。 InnoDB把這些為了回滾而記錄的這些東西稱之為undo log。這里需要注意的一點(diǎn)是,由于查詢操作(SELECT)并不會(huì)修改任何用戶記錄,所以在查詢操作執(zhí)行時(shí),并不需要記錄相應(yīng)的undo log。undo log主要分為3種:
Insert undo log :插入一條記錄時(shí),至少要把這條記錄的主鍵值記下來(lái),之后回滾的時(shí)候只需要把這個(gè)主鍵值對(duì)應(yīng)的記錄刪掉就好了。
Update undo log:修改一條記錄時(shí),至少要把修改這條記錄前的舊值都記錄下來(lái),這樣之后回滾時(shí)再把這條記錄更新為舊值就好了。
Delete undo log:刪除一條記錄時(shí),至少要把這條記錄中的內(nèi)容都記下來(lái),這樣之后回滾時(shí)再把由這些內(nèi)容組成的記錄插入到表中就好了。
- 刪除操作都只是設(shè)置一下老記錄的DELETED_BIT,并不真正將過(guò)時(shí)的記錄刪除。
- 為了節(jié)省磁盤空間,InnoDB有專門的purge線程來(lái)清理DELETED_BIT為true的記錄。為了不影響MVCC的正常工作,purge線程自己也維護(hù)了一個(gè)read view(這個(gè)read view相當(dāng)于系統(tǒng)中最老活躍事務(wù)的read view);如果某個(gè)記錄的DELETED_BIT為true,并且DB_TRX_ID相對(duì)于purge線程的read view可見(jiàn),那么這條記錄一定是可以被安全清除的。
舉個(gè)栗子:
sql | undo log |
---|---|
insert | delete |
delete | insert |
update T set v=3 where v=1 | update T set v=1 where v=3 |
Mysql如何保證持久性
我們了解到InnoDB 為了提升讀寫(xiě)效率,引入了Buffer Pool(緩存池):
- 當(dāng)數(shù)據(jù)庫(kù)讀取數(shù)據(jù)時(shí),會(huì)首先從緩存池中讀取
- 往數(shù)據(jù)庫(kù)寫(xiě)入數(shù)據(jù)時(shí),會(huì)先寫(xiě)入緩存池
- 緩存池中更新的數(shù)據(jù)會(huì)定期刷新到磁盤中
如果MySQL宕機(jī),緩存池中更新的數(shù)據(jù)還沒(méi)有刷回到磁盤中,就會(huì)導(dǎo)致數(shù)據(jù)丟失。于是,redo log被引入進(jìn)來(lái)解決這個(gè)問(wèn)題。
- 先將原始數(shù)據(jù)從磁盤中讀入內(nèi)存中來(lái),修改數(shù)據(jù)的內(nèi)存拷貝。
- 生成一條重做日志并寫(xiě)入redo log buffer,記錄的是數(shù)據(jù)被修改后的值。
- 當(dāng)事務(wù)commit時(shí),將redo log buffer中的內(nèi)容刷新到 redolog file,對(duì) redo log file采用追加寫(xiě)的方式。
- 定期將內(nèi)存中修改的數(shù)據(jù)刷新到磁盤中。
redo與undo在一次事務(wù)操作中是如何交互的?假設(shè)有A、B兩個(gè)數(shù)據(jù),值分別為1、2,開(kāi)啟事務(wù)分別對(duì)其進(jìn)行修改A → 3,B → 4,在提交,過(guò)程如下:
事務(wù) | redo&undo logo |
---|---|
begin; | 開(kāi)啟事務(wù) |
記錄A->3到redo log buffer | |
update T set A=3 where A=1; | A修改為3 |
記錄A=1到undo log | |
記錄B->4到redo log buffer | |
update T set B=4 where B=2; | B修改為4 |
記錄B=2到undo log | |
記錄A->3到redo log記錄B->4到redo log | |
commit; | 事務(wù)提交 |
MySQL怎么保證隔離性
事務(wù)在并發(fā)情形下會(huì)互相干擾到的操作大體可以分為兩類,與之相對(duì)應(yīng)地,MySQL采用了兩種方式來(lái)實(shí)現(xiàn)它們的隔離:
- 一個(gè)事務(wù)的寫(xiě)操作對(duì)另一個(gè)事務(wù)的寫(xiě)操作的影響:鎖機(jī)制保證隔離性
- 一個(gè)事務(wù)的寫(xiě)操作對(duì)另一個(gè)事務(wù)的讀操作的影響:MVCC保證隔離性
加鎖:讀取數(shù)據(jù)之前,對(duì)其加鎖,阻止其他事務(wù)對(duì)數(shù)據(jù)進(jìn)行修改
MVCC:不加任何鎖,采用多版本并發(fā)控制實(shí)現(xiàn),把數(shù)據(jù)庫(kù)的行鎖和行的多個(gè)版本結(jié)合起來(lái),可以實(shí)現(xiàn)非鎖定讀,從而提高數(shù)據(jù)庫(kù)的并發(fā)性能。
事務(wù)隔離級(jí)別
當(dāng)數(shù)據(jù)庫(kù)上有多個(gè)事務(wù)同時(shí)執(zhí)行的時(shí)候,會(huì)帶來(lái)以下問(wèn)題:
問(wèn)題 | 描述 | 舉例 |
---|---|---|
臟讀 | 一個(gè)事務(wù)讀到了另一個(gè)事務(wù)未提交修改的數(shù)據(jù)。 | 事務(wù)A開(kāi)始一個(gè)更新操作,但是還沒(méi)有提交,這時(shí)事務(wù)B讀取了這個(gè)未提交的數(shù)據(jù),就會(huì)產(chǎn)生臟讀。 |
幻讀 | 一個(gè)事務(wù)按相同的查詢條件重新讀取以前檢索過(guò)的數(shù)據(jù),卻發(fā)現(xiàn)其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù)。 | 事務(wù)A進(jìn)行一個(gè)范圍查詢,此時(shí)事務(wù)B插入了一些符合該范圍查詢條件的新數(shù)據(jù),當(dāng)事務(wù)A再次進(jìn)行相同的范圍查詢時(shí),會(huì)發(fā)現(xiàn)多了一些之前沒(méi)有的行,就產(chǎn)生了幻讀。 |
不可重復(fù)讀 | 在一個(gè)事務(wù)中,多次查詢的數(shù)據(jù)不一致。 | 事務(wù)A讀取了一行數(shù)據(jù),然后事務(wù)B對(duì)這一行數(shù)據(jù)進(jìn)行了更新,并且提交了,當(dāng)事務(wù)A再次讀取這一行數(shù)據(jù)時(shí),會(huì)發(fā)現(xiàn)數(shù)據(jù)已經(jīng)發(fā)生了變化,就產(chǎn)生了不可重復(fù)讀。 |
為了避免這些問(wèn)題的出現(xiàn),數(shù)據(jù)庫(kù)引入了隔離級(jí)別的概念,通過(guò)對(duì)不同隔離級(jí)別的設(shè)置,可以控制事務(wù)之間的隔離程度,從而避免并發(fā)問(wèn)題的產(chǎn)生。不同的隔離級(jí)別有不同的特點(diǎn)和使用場(chǎng)景,需要根據(jù)實(shí)際情況進(jìn)行選擇。
以下是四個(gè)標(biāo)準(zhǔn)的事務(wù)隔離級(jí)別:
隔離級(jí)別 | 含義 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
---|---|---|---|---|
讀未提交,Read Uncommitted | 事務(wù)中的修改,即使沒(méi)有提交,對(duì)其他事務(wù)都是可見(jiàn)的 | Y | Y | Y |
讀已提交,Read Committed | 事務(wù)從開(kāi)始到提交之前,所做的修改對(duì)其他事務(wù)都不可見(jiàn) | N | Y | Y |
可重復(fù)讀,Repeatable read | 同一事務(wù)中多次讀取同樣的記錄結(jié)果是一致的 | N | N | Y |
可序列化,Serializable | 在讀取的每一行數(shù)據(jù)上加鎖,強(qiáng)制事務(wù)串行執(zhí)行 | N | N | N |
臟讀的解決
Innodb是通過(guò)在每行數(shù)據(jù)中增加一個(gè)隱藏的事務(wù)ID來(lái)實(shí)現(xiàn)mvcc,當(dāng)一個(gè)事物開(kāi)始時(shí)他會(huì)獲取一個(gè)唯一的事務(wù)ID,該事務(wù)ID用來(lái)標(biāo)記事務(wù)做的修改。當(dāng)事務(wù)讀取一行數(shù)據(jù)時(shí),innodb會(huì)檢查該行數(shù)據(jù)事務(wù)ID是否小于當(dāng)前事務(wù)ID,如果是說(shuō)明該行數(shù)據(jù)是未提交的數(shù)據(jù),innodb會(huì)阻止該事務(wù)讀取該行數(shù)據(jù),從而避免了臟讀的問(wèn)題。
不可重復(fù)讀的解決
innodb通過(guò)mvcc解決不可重復(fù)讀的問(wèn)題,在RR數(shù)據(jù)庫(kù)隔離級(jí)別下,當(dāng)我們使用快照進(jìn)行數(shù)據(jù)讀取的時(shí)候,只會(huì)在第一次讀取的時(shí)候生成一個(gè)ReadView,后續(xù)所有快照讀都是使用同一個(gè)快照,所以就不會(huì)發(fā)生不可重復(fù)讀的問(wèn)題了。
可重復(fù)讀模式下舉個(gè)栗子: 事務(wù)隔離級(jí)別為RR:
創(chuàng)建個(gè)測(cè)試表,并插入一條數(shù)據(jù)(1,1,1)
create table table1( id int(11) not null, ? ? a varchar(50) default null, ? ? b varchar(50) default null, ? ? primary key(id) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
分別開(kāi)啟兩個(gè)事務(wù)測(cè)試:
幻讀的解決
innodb的mvcc和間隙鎖在一定程度上避免了幻讀的發(fā)生,但是沒(méi)有辦法完全避免,當(dāng)一個(gè)事務(wù)讀的時(shí)候會(huì)導(dǎo)致幻讀的發(fā)生。
幻讀的case:
創(chuàng)建一個(gè)用戶表
create table user( id int not null, name varchar(50), age int, primary key(id) );
- 插入幾條數(shù)據(jù)
insert into user values(1,'張三',10),(2,'李四',20),(3,'王二',30);
- 分別開(kāi)啟兩個(gè)事務(wù)測(cè)試:
MVCC實(shí)現(xiàn)
每條記錄在更新的時(shí)候都會(huì)同時(shí)記錄一條回滾操作。同一條記錄在系統(tǒng)中可以存在多個(gè)版本,這就是數(shù)據(jù)庫(kù)的多版本并發(fā)控制(MVCC)。
MySQL中每條記錄,除了我們自定義的字段之外,還有數(shù)據(jù)庫(kù)隱藏定義的三個(gè)字段:
字段 | 描述 |
---|---|
DB_TRX_ID | 6字節(jié),最近修改事務(wù)id,記錄創(chuàng)建這套記錄后者最后一次修改該記錄的事務(wù)id |
DB_ROLL_PTR | 7字節(jié),回滾指針,指向這條記錄的上一個(gè)版本,用于配合undolog |
DB_ROW_ID | 6字節(jié),隱藏的主鍵,如果數(shù)據(jù)表沒(méi)有主鍵,那么innodb會(huì)生成一個(gè)6字節(jié)的row_id |
在 MySQL 中,實(shí)際上每條記錄在更新的時(shí)候都會(huì)同時(shí)記錄一條回滾操作。記錄上的最新值,通過(guò)回滾操作,都可以得到前一個(gè)狀態(tài)的值。
InnoDB 并不會(huì)真正地去開(kāi)辟空間存儲(chǔ)多個(gè)版本的行記錄,只是借助 undo log 記錄每次寫(xiě)操作的反向操作。所以B+ 索引樹(shù)上對(duì)應(yīng)的記錄只會(huì)有一個(gè)最新版本,InnoDB 可以根據(jù) undo log 得到數(shù)據(jù)的歷史版本,從而實(shí)現(xiàn)多版本控制。
Read View
什么是Read View,說(shuō)白了Read View就是事務(wù)進(jìn)行快照讀操作的時(shí)候生產(chǎn)的讀視圖(Read View),在該事務(wù)執(zhí)行的快照讀的那一刻,會(huì)生成數(shù)據(jù)庫(kù)系統(tǒng)當(dāng)前的一個(gè)快照,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)的ID(當(dāng)每個(gè)事務(wù)開(kāi)啟時(shí),都會(huì)被分配一個(gè)ID, 這個(gè)ID是遞增的,所以最新的事務(wù),ID值越大)
所以我們知道 Read View主要是用來(lái)做可見(jiàn)性判斷的, 即當(dāng)我們某個(gè)事務(wù)執(zhí)行快照讀的時(shí)候,對(duì)該記錄創(chuàng)建一個(gè)Read View讀視圖,把它比作條件用來(lái)判斷當(dāng)前事務(wù)能夠看到哪個(gè)版本的數(shù)據(jù),即可能是當(dāng)前最新的數(shù)據(jù),也有可能是該行記錄的undo log里面的某個(gè)版本的數(shù)據(jù)。
Read View遵循一個(gè)可見(jiàn)性算法,主要是將要被修改的數(shù)據(jù)的最新記錄中的DB_TRX_ID(即當(dāng)前事務(wù)ID)取出來(lái),與系統(tǒng)當(dāng)前其他活躍事務(wù)的ID去對(duì)比(由Read View維護(hù)),如果DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見(jiàn)性,那就通過(guò)DB_ROLL_PTR回滾指針去取出Undo Log中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈?zhǔn)椎芥溛?,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID, 那么這個(gè)DB_TRX_ID所在的舊記錄就是當(dāng)前事務(wù)能看見(jiàn)的最新老版本
假設(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ù)庫(kù)的多版本并發(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ì)問(wèn),回滾日志總不能一直保留吧,什么時(shí)候刪除呢?答案是,在不需要的時(shí)候才刪除。也就是說(shuō),系統(tǒng)會(huì)判斷,當(dāng)沒(méi)有事務(wù)再需要用到這些回滾日志時(shí),回滾日志會(huì)被刪除。什么時(shí)候才不需要了呢?就是當(dāng)系統(tǒng)里沒(méi)有比這個(gè)回滾日志更早的 read-view 的時(shí)候。
RC、RR級(jí)別下的InnoDB快照讀有什么不同?
在可重復(fù)讀隔離級(jí)別下,只需要在事務(wù)開(kāi)始的時(shí)候創(chuàng)建一致性視圖,之后事務(wù)里的其他查詢都共用這個(gè)一致性視圖;
在讀提交隔離級(jí)別下,每一個(gè)語(yǔ)句執(zhí)行前都會(huì)重新算出一個(gè)新的視圖。
以上就是深入探究MySQL事務(wù)實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于MySQL 事務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
mysql8 公用表表達(dá)式CTE的使用方法實(shí)例分析
這篇文章主要介紹了mysql8 公用表表達(dá)式CTE的使用方法,結(jié)合實(shí)例形式分析了mysql8 公用表表達(dá)式CTE的基本功能、原理使用方法及相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2020-02-02mysql忘記root密碼的解決辦法(針對(duì)不同mysql版本)
這篇文章主要介紹了mysql忘記root密碼的解決辦法(針對(duì)不同mysql版本),文章通過(guò)代碼示例和圖文結(jié)合的方式給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06簡(jiǎn)單了解MySQL數(shù)據(jù)庫(kù)優(yōu)化技巧
這篇文章主要介紹了簡(jiǎn)單了解MySQL數(shù)據(jù)庫(kù)優(yōu)化技巧,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07mysql數(shù)據(jù)庫(kù)備份命令分享(mysql壓縮數(shù)據(jù)庫(kù)備份)
這篇文章主要介紹了mysql數(shù)據(jù)庫(kù)備份常用語(yǔ)句,包括數(shù)據(jù)庫(kù)壓縮備份、備份多個(gè)MySQL數(shù)據(jù)庫(kù)、備份多個(gè)MySQL數(shù)據(jù)庫(kù)、將數(shù)據(jù)庫(kù)轉(zhuǎn)移到新服務(wù)器等語(yǔ)句2014-01-01Mysql獲取id最大值、表的記錄總數(shù)等相關(guān)問(wèn)題的方法匯總
在做網(wǎng)站開(kāi)發(fā)時(shí),我們也許會(huì)想要取得mysql里id最大的一條記錄,這個(gè)其實(shí)很簡(jiǎn)單。這篇文章給大家整理了獲取一個(gè)表的記錄數(shù)、獲取一個(gè)表的最大id、獲取一個(gè)表的auto_increment值等相關(guān)問(wèn)題的答案,有需要的朋友們可以參考借鑒。2016-09-09linux下啟動(dòng)或者關(guān)閉MySQL數(shù)據(jù)庫(kù)的多種方式
,在Linux服務(wù)器上管理MySQL服務(wù)是一個(gè)基本的運(yùn)維任務(wù),下面這篇文章主要給大家介紹了關(guān)于linux下啟動(dòng)或者關(guān)閉MySQL數(shù)據(jù)庫(kù)的多種方式,文中通過(guò)代碼以及圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06解決Windows環(huán)境下安裝 mysql-8.0.11-winx64 遇到的問(wèn)題
這篇文章主要介紹了Windows環(huán)境下安裝 mysql-8.0.11-winx64 遇到的問(wèn)題及解決辦法 ,需要的朋友可以參考下2018-10-10mysqldump進(jìn)行數(shù)據(jù)備份詳解
這篇文章主要介紹了mysqldump進(jìn)行數(shù)據(jù)備份詳解,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以慘一下2022-07-07