講講MySQL數據庫事務怎么實現的!
什么是數據庫事務
數據庫事務是指一組數據庫操作,這些操作必須被視為一個不可分割的單元,要么全部執行成功,要么全部失敗回滾。事務通常由多個SQL語句組成,這些語句可以讀取、插入、更新或刪除數據庫中的數據。事務具有ACID屬性:
1. 原子性(Atomicity):事務的所有操作被視為單個原子操作,要么全部執行成功,要么全部執行失敗回滾。
2. 一致性(Consistency):事務執行的結果必須使數據庫從一個一致性狀態轉換到另一個一致性狀態,其中包括所有數據完整性和約束性規則的應用。
3. 隔離性(Isolation):一個事務的執行不能被其他并發執行的事務干擾,每個事務應該感覺自己在獨立地執行。
4. 持久性(Durability):一旦事務提交,其結果應該持久保存在數據庫中,即使系統故障也應該如此。
通過實現事務,數據庫系統可以確保數據的完整性和一致性,以及并發訪問時的正確性。如果一個事務中的任何一個操作失敗,整個事務將被回滾到最初的狀態,這確保了數據庫的一致性。
Mysql如何保證原子性
undo log名為回滾日志,是實現原子性的關鍵。InnoDB把這些為了回滾而記錄的這些東西稱之為undo log。這里需要注意的一點是,由于查詢操作(SELECT)并不會修改任何用戶記錄,所以在查詢操作執行時,并不需要記錄相應的undo log。undo log主要分為3種:
? Insert undo log :插入一條記錄時,至少要把這條記錄的主鍵值記下來,之后回滾的時候只需要把這個主鍵值對應的記錄刪掉就好了。
? Update undo log:修改一條記錄時,至少要把修改這條記錄前的舊值都記錄下來,這樣之后回滾時再把這條記錄更新為舊值就好了。
? Delete undo log:刪除一條記錄時,至少要把這條記錄中的內容都記下來,這樣之后回滾時再把由這些內容組成的記錄插入到表中就好了。
? 刪除操作都只是設置一下老記錄的DELETED_BIT,并不真正將過時的記錄刪除。
? 為了節省磁盤空間,InnoDB有專門的purge線程來清理DELETED_BIT為true的記錄。為了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當于系統中最老活躍事務的read view);如果某個記錄的DELETED_BIT為true,并且DB_TRX_ID相對于purge線程的read view可見,那么這條記錄一定是可以被安全清除的。
舉個栗子:
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架構解析我們了解到InnoDB 為了提升讀寫效率,引入了Buffer Pool(緩存池):
- ? 當數據庫讀取數據時,會首先從緩存池中讀取
- ? 往數據庫寫入數據時,會先寫入緩存池
- ? 緩存池中更新的數據會定期刷新到磁盤中
如果MySQL宕機,緩存池中更新的數據還沒有刷回到磁盤中,就會導致數據丟失。于是,redo log被引入進來解決這個問題。
圖片
1. 先將原始數據從磁盤中讀入內存中來,修改數據的內存拷貝。
2. 生成一條重做日志并寫入redo log buffer,記錄的是數據被修改后的值。
3. 當事務commit時,將redo log buffer中的內容刷新到 redolog file,對 redo log file采用追加寫的方式。
4. 定期將內存中修改的數據刷新到磁盤中。
redo與undo在一次事務操作中是如何交互的?假設有A、B兩個數據,值分別為1、2,開啟事務分別對其進行修改A → 3,B → 4,在提交,過程如下:
事務 | redo&undo logo |
begin; | 開啟事務 |
記錄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; | 事務提交 |
MySQL怎么保證隔離性
事務在并發情形下會互相干擾到的操作大體可以分為兩類,與之相對應地,MySQL采用了兩種方式來實現它們的隔離:
1. 一個事務的寫操作對另一個事務的寫操作的影響:鎖機制保證隔離性
2. 一個事務的寫操作對另一個事務的讀操作的影響:MVCC保證隔離性
加鎖:讀取數據之前,對其加鎖,阻止其他事務對數據進行修改
MVCC:不加任何鎖,采用多版本并發控制實現,把數據庫的行鎖和行的多個版本結合起來,可以實現非鎖定讀,從而提高數據庫的并發性能。
事務隔離級別
當數據庫上有多個事務同時執行的時候,會帶來以下問題:
問題 | 描述 | 舉例 |
臟讀 | 一個事務讀到了另一個事務未提交修改的數據。 | 事務A開始一個更新操作,但是還沒有提交,這時事務B讀取了這個未提交的數據,就會產生臟讀。 |
幻讀 | 一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據。 | 事務A進行一個范圍查詢,此時事務B插入了一些符合該范圍查詢條件的新數據,當事務A再次進行相同的范圍查詢時,會發現多了一些之前沒有的行,就產生了幻讀。 |
不可重復讀 | 在一個事務中,多次查詢的數據不一致。 | 事務A讀取了一行數據,然后事務B對這一行數據進行了更新,并且提交了,當事務A再次讀取這一行數據時,會發現數據已經發生了變化,就產生了不可重復讀。 |
為了避免這些問題的出現,數據庫引入了隔離級別的概念,通過對不同隔離級別的設置,可以控制事務之間的隔離程度,從而避免并發問題的產生。不同的隔離級別有不同的特點和使用場景,需要根據實際情況進行選擇。
以下是四個標準的事務隔離級別:
隔離級別 | 含義 | 臟讀 | 不可重復讀 | 幻讀 |
讀未提交,Read Uncommitted | 事務中的修改,即使沒有提交,對其他事務都是可見的 | Y | Y | Y |
讀已提交,Read Committed | 事務從開始到提交之前,所做的修改對其他事務都不可見 | N | Y | Y |
可重復讀,Repeatable read | 同一事務中多次讀取同樣的記錄結果是一致的 | N | N | Y |
可序列化,Serializable | 在讀取的每一行數據上加鎖,強制事務串行執行 | N | N | N |
臟讀的解決
Innodb是通過在每行數據中增加一個隱藏的事務ID來實現mvcc,當一個事物開始時他會獲取一個唯一的事務ID,該事務ID用來標記事務做的修改。當事務讀取一行數據時,innodb會檢查該行數據事務ID是否小于當前事務ID,如果是說明該行數據是未提交的數據,innodb會阻止該事務讀取該行數據,從而避免了臟讀的問題。
不可重復讀的解決
innodb通過mvcc解決不可重復讀的問題,在RR數據庫隔離級別下,當我們使用快照進行數據讀取的時候,只會在第一次讀取的時候生成一個ReadView,后續所有快照讀都是使用同一個快照,所以就不會發生不可重復讀的問題了。
可重復讀模式下舉個栗子:事務隔離級別為RR:
圖片
創建個測試表,并插入一條數據(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;
分別開啟兩個事務測試:
事務1 | 事務2 |
啟動事務,查詢如下: | 啟動事務,查詢如下: |
~ | 將a改為2,可以查到 |
查詢a的結果還是1 | |
~ | 提交事務 |
再次查詢a的結果還是1 | |
提交事務,再次查詢a的結果變為2了 |
幻讀的解決
innodb的mvcc和間隙鎖在一定程度上避免了幻讀的發生,但是沒有辦法完全避免,當一個事務讀的時候會導致幻讀的發生。
幻讀的case:
- ? 創建一個用戶表
create table user(
id int not null,
name varchar(50),
age int,
primary key(id)
);
- ? 插入幾條數據
insert into user values(1,'張三',10),(2,'李四',20),(3,'王二',30);
- ? 分別開啟兩個事務測試:
事務1 | 事務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實現
每條記錄在更新的時候都會同時記錄一條回滾操作。同一條記錄在系統中可以存在多個版本,這就是數據庫的多版本并發控制(MVCC)。
MySQL中每條記錄,除了我們自定義的字段之外,還有數據庫隱藏定義的三個字段:
字段 | 描述 |
DB_TRX_ID | 6字節,最近修改事務id,記錄創建這套記錄后者最后一次修改該記錄的事務id |
DB_ROLL_PTR | 7字節,回滾指針,指向這條記錄的上一個版本,用于配合undolog |
DB_ROW_ID | 6字節,隱藏的主鍵,如果數據表沒有主鍵,那么innodb會生成一個6字節的row_id |
在 MySQL 中,實際上每條記錄在更新的時候都會同時記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。
InnoDB 并不會真正地去開辟空間存儲多個版本的行記錄,只是借助 undo log 記錄每次寫操作的反向操作。所以B+ 索引樹上對應的記錄只會有一個最新版本,InnoDB 可以根據 undo log 得到數據的歷史版本,從而實現多版本控制。
Read View
什么是Read View,說白了Read View就是事務進行快照讀操作的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)
所以我們知道 Read View主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄創建一個Read View讀視圖,把它比作條件用來判斷當前事務能夠看到哪個版本的數據,即可能是當前最新的數據,也有可能是該行記錄的undo log里面的某個版本的數據。
Read View遵循一個可見性算法,主要是將要被修改的數據的最新記錄中的DB_TRX_ID(即當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由Read View維護),如果DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見性,那就通過DB_ROLL_PTR回滾指針去取出Undo Log中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID, 那么這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本
假設一個值從 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 的時候。
那么RC、RR級別下的InnoDB快照讀有什么不同?
在可重復讀隔離級別下,只需要在事務開始的時候創建一致性視圖,之后事務里的其他查詢都共用這個一致性視圖;
在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的視圖。