一、前言
CURD 不加控制,會有什么問題?
CURD 滿足什么屬性,能解決上述問題?
- 買票的過程得是原子的。
- 買票應該不能受互相的影響。
- 買完票應該要永久有效。
- 買前和買后都要是確定的狀態。
什么是事務?
- 事務就是一組?DML?語句組成,這些語句在邏輯上存在相關性,這一組?DML?語句要么全部成功,要么全部失敗,是一個整體。MySQL?提供一種機制,保證我們達到這樣的效果。事務還規定不同的客戶端看到的數據是不相同的。
- 事務就是要做的或所做的事情,主要用于處理操作量大、復雜度高的數據。假設一種場景:你畢業了,學校的教務系統后臺 MySQL 中,不再需要你的數據,要刪除你的所有有關信息(一般不會),那么要刪除你的基本信息(姓名、電話、籍貫等)的同時,也刪除和你有關的其他信息,比如:你的各科成績,你的在校表現,甚至你在論壇發過的帖子、文章等。這樣就需要多條 MySQL 語句構成,那么所有這些操作合起來就構成了一個事務。
- 正如上面所說,一個 MySQL 數據庫,可不止你一個事務在運行,同一時刻甚至有大量的請求被包裝成事務,再向 MySQL 服務器發起事務處理請求。而每條事務至少一條?SQL,最多很多 SQL,這樣如果大家都訪問同樣的表數據,在不加保護的情況下就絕對會出現問題。甚至因為事務由多條 SQL 構成,那么也會存在執行到一半出錯或者不想再執行的情況,那么已經執行的怎么辦呢?
一個完整的事務,絕對不是簡單的 sql 集合,還需要滿足如下四個屬性:
- 原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- 一致性:在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。
- 隔離性:數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable )。
- 持久性:事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
- 原子性(Atomicity,或稱不可分割性)
- 一致性(Consistency)
- 隔離性(Isolation,又稱獨立性)
- 持久性(Durability)
為什么會出現事務?
事務被 MySQL 編寫者設計出來, 本質是為了當應用程序訪問數據庫時, 事務能夠簡化我們的編程模型, 不需要我們去考慮各種各樣的潛在錯誤和并發問題。 可以想一下當我們使用事務時, 要么提交, 要么回滾, 我們不會去考慮網絡異常了, 服務器宕機了, 同時更改一個數據該怎么辦。因此事務本質上是 為了應用層服務 的。 而不是伴隨著數據庫系統天生就有的。(提示:我們后面把 MySQL 中的一行信息,稱為一行記錄。)
二、事務的版本支持
在 MySQL 中只有使用了 Innodb 數據庫引擎的數據庫或表才支持事務, MyISAM 不支持。
1、查看數據庫引擎
(1)表格顯示
(2) 行顯示
三、事務提交方式
1、事務的提交方式
常見的事務的提交方式有兩種:
- 自動提交
- 手動提交
2、查看事務提交方式
- 用 set?來改變 MySQL 的自動提交模式:
四、事務常見操作方式
1、練習 ——?簡單銀行用戶表
(1)提前準備
- Centos 7 云服務器,默認開啟 3306 mysqld 服務

-
使用 Win cmd 遠程訪問 Centos 7 云服務器,mysqld 服務(需要 Win 上也安裝了 MySQL,這里看到結果即可) - 注意:使用本地 MySQL 客戶端可能看不到鏈接效果,本地可能使用域間套接字查不到鏈接。
C:\Users\whb>mysql -uroot -p -h42.192.83.143
Enter password: ***********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3484
Server version: 5.7.33 MySQL Community Server (GPL)Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

-
使用 netstat 查看鏈接情況,可知:MySQL?本質是一個客戶端進程
-
為了便于演示,我們將 MySQL?的默認隔離級別設置成讀未提交。
(2)創建測試表
(3)正常演示?——?證明事務的開始與回滾
(4)非正常演示
A.?證明未?commit,客戶端崩潰,MySQL?自動會回滾(隔離級別設置為讀未提交)
- 終端 A
-
終端 B
B. 證明?commit?了,客戶端崩潰,MySQL?數據不會在受影響,已經持久化
- 終端 A
-
終端 B
C. 對比試驗。證明?begin?操作會自動更改提交方式,不會受?MySQL?是否自動提交影響
- 終端 A
-
終端 B
D. 證明單條 SQL 與事務的關系
a. 實驗一
- 終端 A
-
終端 B
b. 實驗二
- 終端 A
-
終端 B
2、結論
- 只要輸入 begin /?start transaction,事務便必須要通過 commit 提交,才會持久化,與是否設置 set autocommit 無關。
- 事務可以手動回滾,同時,當操作異常,MySQL 會自動回滾。
- 對于 InnoDB 每一條 SQL 語言都默認封裝成事務,自動提交。(select 有特殊情況,因為 MySQL 有 MVCC )
- 從上面的例子,我們能看到事務本身的原子性(回滾),持久性(commit)。
3、事務操作注意事項
- 如果沒有設置保存點,也可以回滾,但只能回滾到事務的開始。直接使用 rollback(前提是事務還沒有提交)。
- 如果一個事務被提交了(commit),則不可以回退(rollback)。
- 可以選擇回退到哪個保存點。
- InnoDB 支持事務, MyISAM 不支持事務。
- 開始事務可以用?start transaction /?begin。
五、事務隔離級別
1、如何理解隔離性
- MySQL?服務可能會同時被多個客戶端進程(線程)訪問,訪問的方式以事務方式進行。
- 一個事務可能由多條?SQL?構成,也就意味著任何一個事務都有執行前、執行中、執行后的階段。而所謂的原子性,其實就是讓用戶層要么看到執行前,要么看到執行后,執行中出現問題可以隨時回滾。所以單個事務對用戶表現出來的特性就是原子性。
- 但畢竟所有事務都要有個執行過程,那么在多個事務各自執行多個?SQL?時,就還是有可能會出現互相影響的情況。比如:多個事務同時訪問同一張表,甚至同一行數據。
- 假設你媽媽給你說:你要么別學,要學就學到最好。至于你怎么學,中間有什么困難,你的媽媽并不關心。那么你的學習對于你媽媽來說,就是原子的。那么你的學習過程很容易受別人干擾,此時就需要將你的學習隔離開,以保證你的學習環境是健康的。
- 在數據庫中,為了保證事務執行過程中盡量不受干擾,就有了一個重要特征:隔離性。
- 在數據庫中,允許事務受不同程度的干擾,就有了一種重要特征:隔離級別。
2、隔離級別
- 讀未提交【Read Uncommitted】在該隔離級別,所有的事務都可以看到其他事務沒有提交的執行結果。(實際生產中不可能使用這種隔離級別的)但相當于沒有任何隔離性,也會有很多并發問題,如臟讀、幻讀、不可重復讀等,我們前面為了做實驗方便,用的就是這個隔離性。
- 讀提交【Read Committed】該隔離級別是大多數據庫的默認的隔離級別(不是?MySQL 默認的)。它滿足了隔離的簡單定義:一個事務只能看到其他的已經提交的事務所做的改變。這種隔離級別會引起不可重復讀,即一個事務執行時,如果多次 select,可能得到不同的結果。
- 可重復讀【Repeatable Read】這是 MySQL 默認的隔離級別,它確保同一個事務在執行中,多次讀取操作數據時,會看到同樣的數據行,但是會出現幻讀問題。
- 串行化【Serializable】這是事務的最高隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決了幻讀的問題。它在每個讀的數據行上面加上共享鎖,但可能會導致超時和鎖競爭(這種隔離級別太極端,實際生產基本不使用)。
3、查看與設置隔離性
(1)查看
(2)設置
- 設置當前會話 /?全局隔離級別
set [session?| global] transaction isolation level {read uncommitted | read committed |?repeatable read | serializable} ;
注意:改變當前會話隔離級別,重啟不受影響。但改變全局隔離級別的話,重啟之后默認值會發生改變。
-
設置當前會話隔離性,另起一個會話,看不多,只影響當前會話
串行化:set session transaction isolation level serializable;
- 設置全局隔離性,另起一個會話,會被影響
set global transaction isolation level read uncommitted;
注意:如果沒有現象,關閉 M ySQL? 客戶端,重新連接。
4、讀未提交【Read Uncommitted】?
幾乎沒有加鎖,雖然效率高,但是問題太多,嚴重不建議采用。
- 終端 A
-
終端 B
一個事務在執行中,讀到另一個執行中事務的更新或其他操作,但是未?commit?的數據,這種現象叫做臟讀(dirty read)。?
5、?讀提交【Read Committed】
- 終端 A
-
終端 B
終端 B 進行第 2 次 select * from account 之前,此時還在當前事務中,并未? commit ,那么就造成了在同一個事務內進行同樣的讀取,在不同的時間段( 依舊還在事務操作中) ,讀取到了不同的值,這種現象叫做不可重復讀( non reapeatable read)
6、可重復讀【Repeatable Read】
- 終端 A
-
終端 B
在終端 B commit 之前可以看到, 事務無論什么時候進行查找,看到的結果都是一致的,這叫做可重復讀。
如果將上面的終端 A 中的 update 操作,改成 insert 操作,會有什么問題?
- 終端 A
-
終端 B
select * from account;? 多次查看,發現終端? A? 在對應事務中? insert? 的數據在終端? B? 的事務周期中,也沒有什么影響,也符合可重復的特點。但一般的數據庫在可重復讀情況的時候,無法屏蔽其他事務 insert? 的數據。為什么?因為隔離性實現是對數據加鎖完成的,而 insert? 待插入的數據因為并不存在,那么一般加鎖無法屏蔽這類問題, 會造成雖然大部分內容是可重復讀的,但是 insert? 的數據在可重復讀情況被讀取出來,導致多次查找時會多查找出來新的記錄,就如同產生了幻覺。這種現象,叫做幻讀(phantom read) 。很明顯, MySQL? 在? RR? 級別的時候,是解決了幻讀問題的( 解決的方式是用 Next-Key 鎖( GAP+ 行鎖) 解決的。
?7、串行化【serializable】?
對所有操作全部加鎖,進行串行化不會有問題,但是只要串行化,效率就很低,幾乎完全不會被采用。
- 終端 A?
-
終端 B
8、總結
- 其中隔離級別越嚴格,安全性越高,但數據庫的并發性能也就越低,往往需要在兩者之間找一個平衡點。
- 不可重復讀的重點是修改和刪除:同樣的條件, 你讀取過的數據,再次讀取出來發現值不一樣了 幻讀的重點在于新增:同樣的條件在第?1?次和第?2?次讀出來的記錄數不一樣。
- 說明:MySQL?默認的隔離級別是可重復讀,一般情況下不要修改。
- 上面的例子可以看出,事務也有長短事務這樣的概念。事務間互相影響,指的是事務在并行執行的時候,即都沒有 commit?的時候,影響會比較大。
六、一致性(Consistency)
- 事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。當數據庫只包含事務成功提交的結果時,數據庫處于一致性狀態。如果系統運行發生中斷,某個事務尚未完成而被迫中斷,而改未完成的事務對數據庫所做的修改已被寫入數據庫,此時數據庫就處于一種不正確(不一致)的狀態。因此一致性是通過原子性來保證的。
- 其實一致性和用戶的業務邏輯強相關,一般?MySQL?提供技術支持,但是一致性還是要用戶業務邏輯做支撐,也就是一致性是由用戶決定的。
- 而技術上,通過 AID?保證?C。
七、擴展學習(了解)
如何實現事務的隔離性-CSDN博客
Innodb 中的事務隔離級別和鎖的關系-CSDN博客
Mysql 間隙鎖原理,以及 Repeatable Read 隔離級別下可以防止幻讀原理-CSDN博客
在?RR?級別的時候,多個事務的?update,多個事務的?insert,多個事務的?delete,是否會有加鎖現象?
現象結果是, update , insert , delete? 之間是會有加鎖現象的,但是? select? 和這些操作是不沖突的。這就是通過讀寫鎖(鎖有行鎖 / 表鎖) + MVCC? 完成隔離性。
八、如何理解隔離性(深度)
數據庫并發的場景有三種:
- 讀-讀 :不存在任何問題,也不需要并發控制。
- 讀-寫 :有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀、幻讀、不可重復讀。
- 寫-寫 :有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失(后面補充)。
?1、讀-寫?
- 在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫并發讀寫的性能。
- 同時還可以解決臟讀、幻讀、不可重復讀等事務隔離問題,但不能解決更新丟失問題。
- 每個事務都要有自己的事務 ID,可以根據事務 ID 的大小來決定事務到來的先后順序。
- mysqld 可能會面臨處理多個事務的情況,事務也有自己的生命周期,mysqld 要對多個事務進行管理,先描述,再組織。事務在我看來,mysqld 中一定是對應的一個或者一套結構體對象 / 類對象,事務也要有自己的結構體。
- 3?個記錄隱藏字段
- undo 日志
- Read View
(1)3?個記錄隱藏列字段
- DB_TRX_ID:6 byte,最近修改(修改?/?插入)事務?ID,記錄創建這條記錄?/?最后一次修改該記錄的事務?ID。
- DB_ROLL_PTR:7 byte,回滾指針,指向這條記錄的上一個版本(簡單理解成指向歷史版本就行,這些數據一般在 undo log 中)。
- DB_ROW_ID:6 byte,隱含的自增?ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB 會自動以 DB_ROW_ID 產生一個聚簇索引。
補充:實際還有一個刪除?flag?隱藏字段,既記錄被更新或刪除并不代表真的刪除,而是刪除?flag?變了

(2)undo 日志
MySQL 將來是以服務進程的方式,在內存中運行。之前所學的所有機制:索引、事務、隔離性、日志等都是在內存中完成的,即在 MySQL 內部的相關緩沖區中保存相關數據,完成各種判斷操作。然后在合適的時候將相關數據刷新到磁盤當中的。所以,理解? undo log ? 簡單理解成就是 MySQL 中的一段 內存緩沖區 ,用來保存日志數據的就行。
(3)模擬 MVCC
- 事務?10?因為要修改,所以要先給該記錄加行鎖。
- 修改前,現將改行記錄拷貝到?undo log?中。所以,undo log?中就有了一行副本數據。(原理就是寫時拷貝)
- 所以現在 MySQL 中有兩行同樣的記錄。現在修改原始記錄中的?name,改成 '李四',并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前 事務?10 的?ID,我們默認從 10 開始,之后遞增。而原始記錄的回滾指針 DB_ROLL_PTR 列,里面寫入 undo log?中副本數據的地址,從而指向副本記錄,表示我的上一個版本就是它。
- 事務?10?提交,釋放鎖。?
- 事務?11?因為也要修改,所以要先給該記錄加行鎖。
- 修改前,現將改行記錄拷貝到?undo log?中。所以,undo log?中就又有了一行副本數據。此時,新的副本我們采用頭插方式,插入?undo log。
- 現在修改原始記錄中的?age?改成 38,并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前事務?11 的?ID。而原始記錄的回滾指針 DB_ROLL_PTR 列里面寫入?undo log?中副本數據的地址,從而指向副本記錄,既表示我的上一個版本就是它。
- 事務11提交,釋放鎖。
這樣,就有了一個基于鏈表記錄的歷史版本鏈。所謂的回滾無非就是用歷史數據覆蓋當前數據。 ?
上面的一個個版本,我們可以稱之為一個個的 快照 。
【思考】
上面是以更新(upadte)主講的,如果是?delete?呢?
一樣的,別忘了,刪數據不是清空,而是設置? flag? 為刪除即可,也可以形成版本。
如果是?insert?呢?
因為? insert? 是插入,也就是之前沒有數據,那么? insert? 也就沒有歷史版本。但是一般為了回滾操作,insert? 的數據也是要被放入? undo log? 中,如果當前事務? commit? 了,那么這個? undo log 的歷史? insert? 記錄就可以被清空了。
那么 select 呢?
select? 不會對數據做任何修改,所以為? select? 維護多版本沒有意義。
此時有個問題,就是 select 讀取,是讀取最新的版本呢?還是讀取歷史版本?
- 當前讀:讀取最新的記錄,就是當前讀。增刪改,都叫做當前讀,select?也有可能當前讀,比如:select lock in share mode(共享鎖),select for update。
- 快照讀:讀取歷史版本(一般而言),就叫做快照讀。(這個后面重點討論)
可以看到,在多個事務同時刪改查時都是當前讀,是要加鎖的。那同時有? select? 過來,如果也要讀取最新版( 當前讀) ,那么也就需要加鎖,這就是串行化。但如果是快照讀,讀取歷史版本的話,是不受加鎖限制的,也就是可以并行執行。換而言之,提高了效率,即? MVCC? 的意義所在。
那么是什么決定了?select?是當前讀還是快照讀呢?
隔離級別。
那為什么要有隔離級別呢?
事務都是原子的。所以無論如何,事務總有先有后。
先來的事務應不應該看到后來的事務所做的修改呢? 那么如何保證不同的事務看到不同的內容呢?也就是如何實現隔離級別?
(4)Read View
Read View ? 就是事務進行 快照讀 操作的時候生產的 讀視圖 ( Read View) ,在該事務執行的快照讀的那一刻會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的 ID( 當每個事務開啟時都會被分配一個? ID, 這個? ID? 是遞增的,所以最新的事務 ID? 值越大)Read View 在 MySQL 源碼中 就是一個 類 ,本質是用來進行可見性判斷的。 即當我們某個事務執行快照讀的時候,對該記錄創建一個 Read View 讀視圖,把它比作條件 用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的 undo log 里面的某個版本的數據。
下面是簡化過的?ReadView 結構:
class ReadView {// ...private:// 高水位,大于等于這個ID的事務均不可見trx_id_t m_low_limit_id// 低水位:小于這個ID的事務均可見trx_id_t m_up_limit_id;// 創建該 Read View 的事務IDtrx_id_t m_creator_trx_id;// 創建視圖時的活躍事務id列表ids_t m_ids;// 配合purge,標識該視圖不需要小于m_low_limit_no的UNDO LOG// 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOGtrx_id_t m_low_limit_no;// 標記視圖是否被關閉bool m_closed;// ...
};
m_ids; // 一張列表,用來維護Read View生成時刻,系統正活躍的事務IDup_limit_id; // 記錄m_ids列表中事務ID最小的IDlow_limit_id; // ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1creator_trx_id; // 創建該ReadView的事務ID
Read View 是事務可見性的一個類,不識事務創建出來就有的,而是當這個事務(已經存在)首次進行快照讀時,MySQL 形成的。
對應源碼策略: ?
如果查到不應該看到當前版本,接下來就是遍歷下一個版本,直到符合條件就可以看到。上面的? readview 是當你進行 select? 時會自動形成。
(5)整體流程
- 假設當前有條記錄:

- ?事務操作:
事務? 4 :修改? name( 張三 ) 變成? name( 李四 )。當 事務? 2 對某行數據執行了 快照讀 ,數據庫為該行數據生成一個 Read View 讀視圖。
// 事務2的 Read View
m_ids; // 1, 3
up_limit_id; // 1
low_limit_id; // 4+1=5,原因:ReadView生成時刻,系統尚未分配的下一個事務ID
creator_trx_id; // 2
- 此時版本鏈是: ?
- 只有事務? 4? 修改過該行記錄,并在事務? 2? 執行快照讀前,就提交了事務。

- 事務?2?在快照讀該行記錄時會拿該行記錄的 DB_TRX_ID 去跟 up_limit_id,low_limit_id?和活 躍事務? ID? 列表 (trx_list) 進行比較,判斷當前事務? 2? 能看到該記錄的版本。
// 事務2的 Read View
m_ids; // 1, 3
up_limit_id; // 1
low_limit_id; // 4+1=5,原因:ReadView生成時刻,系統尚未分配的下一個事務ID
creator_trx_id; // 2// 事務4提交的記錄對應的事務ID
DB_TRX_ID=4// 比較步驟
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,說明,事務4不在當前的活躍事務中。// 結論
事務4的更改應該看到。
所以事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本
(6)RR 與 RC?的本質區別
a. 當前讀和快照讀在?RR?級別下的區別
select * from user lock in share mode?以加共享鎖方式進行讀取,對應的就是當前讀。
- 測試用例1 -?表1:?
- 測試用例2 -?表2:
- 用例?1?與用例?2?唯一區別僅僅是表?1?的事務?B?在事務?A?修改?age?前快照讀過一次?age?數據。
- 而表?2?的事務?B?在事務?A?修改?age?前沒有進行過快照讀。
【結論】
- 事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,即某個事務中首次出現快照讀,決定該事務后續快照讀結果的能力 delete 也同樣如此
【總結 —— RR 與 RC?的本質區別】
- 正是?Read View?生成時機的不同,從而造成?RC 和 RR?級別下快照讀的結果的不同。
- 在?RR?級別下的某個事務的對某條記錄的第一次快照讀會創建一個快照及?Read View?將當前系統活躍的其他事務記錄起來。
- 此后在調用快照讀時,使用的還是同一個??Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那么之后的快照讀使用的都是同一個?Read View,所以對之后的修改不可見。
- 即?RR?級別下,快照讀生成?Read View?時,Read View?會記錄此時所有其他活動事務的快照,這些事務的修改對于當前事務都是不可見的。而早于?Read View?創建的事務所做的修改均是可見的。
- 而在?RC?級別下的,事務中,每次快照讀都會新生成一個快照和?Read View,這就是我們在?RC?級別下的事務中可以看到別的事務提交的更新的原因。
- 總之在?RC?隔離級別下,是每個快照讀都會生成并獲取最新的?Read View;而在?RR?隔離級別下,則是同一個事務中的第一個快照讀才會創建?Read View,之后的快照讀獲取的都是同一個?Read View。
- 正是?RC?每次快照讀,都會形成?Read View,所以?RC?才會有不可重復讀問題。
2、讀-讀
不討論。
?3、寫-寫 ?
現階段直接理解成都是當前讀,這里不做深究。
4、推薦閱讀
【MySQL筆記】正確的理解MySQL的MVCC及實現原理_mysqlmvcc實現原理-CSDN博客
詳細分析MySQL事務日志(redo log和undo log) - 駿馬金龍 - 博客園 (cnblogs.com)
【MySQL】InnoDB 如何避免臟讀和不可重復讀_innodb怎么解決臟讀-CSDN博客