講講MySQL數(shù)據(jù)庫(kù)事務(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屬性:
1. 原子性(Atomicity):事務(wù)的所有操作被視為單個(gè)原子操作,要么全部執(zhí)行成功,要么全部執(zhí)行失敗回滾。
2. 一致性(Consistency):事務(wù)執(zhí)行的結(jié)果必須使數(shù)據(jù)庫(kù)從一個(gè)一致性狀態(tài)轉(zhuǎn)換到另一個(gè)一致性狀態(tài),其中包括所有數(shù)據(jù)完整性和約束性規(guī)則的應(yīng)用。
3. 隔離性(Isolation):一個(gè)事務(wù)的執(zhí)行不能被其他并發(fā)執(zhí)行的事務(wù)干擾,每個(gè)事務(wù)應(yīng)該感覺(jué)自己在獨(dú)立地執(zhí)行。
4. 持久性(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把這些為了回滾而記錄的這些東西稱(chēng)之為undo log。這里需要注意的一點(diǎn)是,由于查詢(xún)操作(SELECT)并不會(huì)修改任何用戶(hù)記錄,所以在查詢(xún)操作執(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é)省磁盤(pán)空間,InnoDB有專(zhuān)門(mén)的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如何保證持久性
通過(guò)Innodb架構(gòu)解析我們了解到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ì)定期刷新到磁盤(pán)中
如果MySQL宕機(jī),緩存池中更新的數(shù)據(jù)還沒(méi)有刷回到磁盤(pán)中,就會(huì)導(dǎo)致數(shù)據(jù)丟失。于是,redo log被引入進(jìn)來(lái)解決這個(gè)問(wèn)題。
圖片
1. 先將原始數(shù)據(jù)從磁盤(pán)中讀入內(nèi)存中來(lái),修改數(shù)據(jù)的內(nèi)存拷貝。
2. 生成一條重做日志并寫(xiě)入redo log buffer,記錄的是數(shù)據(jù)被修改后的值。
3. 當(dāng)事務(wù)commit時(shí),將redo log buffer中的內(nèi)容刷新到 redolog file,對(duì) redo log file采用追加寫(xiě)的方式。
4. 定期將內(nèi)存中修改的數(shù)據(jù)刷新到磁盤(pán)中。
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ì)互相干擾到的操作大體可以分為兩類(lèi),與之相對(duì)應(yīng)地,MySQL采用了兩種方式來(lái)實(shí)現(xiàn)它們的隔離:
1. 一個(gè)事務(wù)的寫(xiě)操作對(duì)另一個(gè)事務(wù)的寫(xiě)操作的影響:鎖機(jī)制保證隔離性
2. 一個(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ù)按相同的查詢(xún)條件重新讀取以前檢索過(guò)的數(shù)據(jù),卻發(fā)現(xiàn)其他事務(wù)插入了滿(mǎn)足其查詢(xún)條件的新數(shù)據(jù)。 | 事務(wù)A進(jìn)行一個(gè)范圍查詢(xún),此時(shí)事務(wù)B插入了一些符合該范圍查詢(xún)條件的新數(shù)據(jù),當(dāng)事務(wù)A再次進(jìn)行相同的范圍查詢(xún)時(shí),會(huì)發(fā)現(xiàn)多了一些之前沒(méi)有的行,就產(chǎn)生了幻讀。 |
不可重復(fù)讀 | 在一個(gè)事務(wù)中,多次查詢(xún)的數(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è)試:
事務(wù)1 | 事務(wù)2 |
啟動(dòng)事務(wù),查詢(xún)?nèi)缦拢?img src="https://s2.51cto.com/oss/202404/08/08aef8338b0d9ec8114602ca986994fc73424a.png" alt="圖片" title="圖片" style="visibility: visible; width: 313px;" data-type="inline"> | 啟動(dòng)事務(wù),查詢(xún)?nèi)缦拢?img src="https://s4.51cto.com/oss/202404/08/67856ef763a74c46d45812e1817ec5379bc98d.png" alt="圖片" title="圖片" style="visibility: visible; width: 313px;" data-type="inline"> |
~ | 將a改為2,可以查到 |
查詢(xún)a的結(jié)果還是1 | |
~ | 提交事務(wù) |
再次查詢(xún)a的結(jié)果還是1 | |
提交事務(wù),再次查詢(xún)a的結(jié)果變?yōu)?了 |
幻讀的解決
innodb的mvcc和間隙鎖在一定程度上避免了幻讀的發(fā)生,但是沒(méi)有辦法完全避免,當(dāng)一個(gè)事務(wù)讀的時(shí)候會(huì)導(dǎo)致幻讀的發(fā)生。
幻讀的case:
- ? 創(chuàng)建一個(gè)用戶(hù)表
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è)試:
事務(wù)1 | 事務(wù)2 |
begin;select * from user where age >10 and age<40; | |
begin;insert into user value(4,'麻子',25); commit; | |
select * from user where age >10 and age<40; | |
update user set name='呵呵' where age=25;select * from user where age >10 and age<40; |
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)椎芥溛?,即從最近的一次修改查起),直到找到滿(mǎn)足特定條件的DB_TRX_ID, 那么這個(gè)DB_TRX_ID所在的舊記錄就是當(dāng)前事務(wù)能看見(jiàn)的最新老版本
假設(shè)一個(gè)值從 1 被按順序改成了 2、3、4,在回滾日志里面就會(huì)有類(lèi)似下面的記錄。
圖片
當(dāng)前值是 4,但是在查詢(xún)這條記錄的時(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ù)里的其他查詢(xún)都共用這個(gè)一致性視圖;
在讀提交隔離級(jí)別下,每一個(gè)語(yǔ)句執(zhí)行前都會(huì)重新算出一個(gè)新的視圖。