《包你懂系列》一文講清楚 MySQL 事務隔離級別和實現原理,開發人員必備知識點

我是風箏,公衆號「古時的風箏」,一個不僅有技術的技術公衆號,一個在程序圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。 Spring Cloud 系列文章已經完成,能夠到 個人github 上查看系列完整內容。也能夠在公衆號內回覆「pdf」獲取我精心製做的 pdf 版完整教程。mysql

常常提到數據庫的事務,那你知道數據庫還有事務隔離的說法嗎,事務隔離還有隔離級別,那什麼是事務隔離,隔離級別又是什麼呢?本文就幫你們梳理一下。git

MySQL 事務

本文所說的 MySQL 事務都是指在 InnoDB 引擎下,MyISAM 引擎是不支持事務的。github

數據庫事務指的是一組數據操做,事務內的操做要麼就是所有成功,要麼就是所有失敗,什麼都不作,其實不是沒作,是可能作了一部分可是隻要有一步失敗,就要回滾全部操做,有點一不作二不休的意思。web

假設一個網購付款的操做,用戶付款後要涉及到訂單狀態更新、扣庫存以及其餘一系列動做,這就是一個事務,若是一切正常那就相安無事,一旦中間有某個環節異常,那整個事務就要回滾,總不能更新了訂單狀態可是不扣庫存吧,這問題就大了。spring

事務具備原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability)四個特性,簡稱 ACID,缺一不可。今天要說的就是隔離性sql

概念說明

如下幾個概念是事務隔離級別要實際解決的問題,因此須要搞清楚都是什麼意思。shell

髒讀

髒讀指的是讀到了其餘事務未提交的數據,未提交意味着這些數據可能會回滾,也就是可能最終不會存到數據庫中,也就是不存在的數據。讀到了並必定最終存在的數據,這就是髒讀。數據庫

可重複讀

可重複讀指的是在一個事務內,最開始讀到的數據和事務結束前的任意時刻讀到的同一批數據都是一致的。一般針對數據**更新(UPDATE)**操做。ruby

不可重複讀

對比可重複讀,不可重複讀指的是在同一事務內,不一樣的時刻讀到的同一批數據多是不同的,可能會受到其餘事務的影響,好比其餘事務改了這批數據並提交了。一般針對數據**更新(UPDATE)**操做。bash

幻讀

幻讀是針對數據**插入(INSERT)**操做來講的。假設事務A對某些行的內容做了更改,可是還未提交,此時事務B插入了與事務A更改前的記錄相同的記錄行,而且在事務A提交以前先提交了,而這時,在事務A中查詢,會發現好像剛剛的更改對於某些數據未起做用,但實際上是事務B剛插入進來的,讓用戶感受很魔幻,感受出現了幻覺,這就叫幻讀。

事務隔離級別

SQL 標準定義了四種隔離級別,MySQL 全都支持。這四種隔離級別分別是:

  1. 讀未提交(READ UNCOMMITTED)

  2. 讀提交 (READ COMMITTED)

  3. 可重複讀 (REPEATABLE READ)

  4. 串行化 (SERIALIZABLE)

從上往下,隔離強度逐漸加強,性能逐漸變差。採用哪一種隔離級別要根據系統需求權衡決定,其中,可重複讀是 MySQL 的默認級別。

事務隔離其實就是爲了解決上面提到的髒讀、不可重複讀、幻讀這幾個問題,下面展現了 4 種隔離級別對這三個問題的解決程度。

隔離級別 髒讀 不可重複讀 幻讀
讀未提交 可能 可能 可能
讀提交 不可能 可能 可能
可重複讀 不可能 不可能 可能
串行化 不可能 不可能 不可能

只有串行化的隔離級別解決了所有這 3 個問題,其餘的 3 個隔離級別都有缺陷。

一探究竟

下面,咱們來一一分析這 4 種隔離級別究竟是怎麼個意思。

如何設置隔離級別

咱們能夠經過如下語句查看當前數據庫的隔離級別,經過下面語句能夠看出我使用的 MySQL 的隔離級別是 REPEATABLE-READ,也就是可重複讀,這也是 MySQL 的默認級別。

# 查看事務隔離級別 5.7.20 以後
show variables like 'transaction_isolation';
SELECT @@transaction_isolation

# 5.7.20 以後
SELECT @@tx_isolation
show variables like 'tx_isolation'

+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
複製代碼

稍後,咱們要修改數據庫的隔離級別,因此先了解一下具體的修改方式。

修改隔離級別的語句是:set [做用域] transaction isolation level [事務隔離級別], SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL READ UNCOMMITTED READ COMMITTED | REPEATABLE READ | SERIALIZABLE

其中做用於能夠是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只針對當前回話窗口。隔離級別是 READ UNCOMMITTED READ COMMITTED | REPEATABLE READ | SERIALIZABLE 這四種,不區分大小寫。

好比下面這個語句的意思是設置全局隔離級別爲讀提交級別。

mysql> set global transaction isolation level read committed; 
複製代碼

MySQL 中執行事務

事務的執行過程以下,以 begin 或者 start transaction 開始,而後執行一系列操做,最後要執行 commit 操做,事務纔算結束。固然,若是進行回滾操做(rollback),事務也會結束。

須要注意的是,begin 命令並不表明事務的開始,事務開始於 begin 命令以後的第一條語句執行的時候。例以下面示例中,select * from xxx 纔是事務的開始,

begin;
select * from xxx;
commit; -- 或者 rollback;
複製代碼

另外,經過如下語句能夠查詢當前有多少事務正在運行。

select * from information_schema.innodb_trx;
複製代碼

好了,重點來了,開始分析這幾個隔離級別了。

接下來我會用一張表來作一下驗證,表結構簡單以下:

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) DEFAULT NULL,
`age` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
複製代碼

初始只有一條記錄:

mysql> SELECT * FROM user;
+----+-----------------+------+
| id | name | age |
+----+-----------------+------+
| 1 | 古時的風箏 | 1 |
+----+-----------------+------+
複製代碼

讀未提交

MySQL 事務隔離實際上是依靠鎖來實現的,加鎖天然會帶來性能的損失。而讀未提交隔離級別是不加鎖的,因此它的性能是最好的,沒有加鎖、解鎖帶來的性能開銷。但有利就有弊,這基本上就至關於裸奔啊,因此它連髒讀的問題都沒辦法解決。

任何事務對數據的修改都會第一時間暴露給其餘事務,即便事務尚未提交。

下面來作個簡單實驗驗證一下,首先設置全局隔離級別爲讀未提交。

set global transaction isolation level read uncommitted;
複製代碼

設置完成後,只對以後新起的 session 才起做用,對已經啓動 session 無效。若是用 shell 客戶端那就要從新鏈接 MySQL,若是用 Navicat 那就要建立新的查詢窗口。

啓動兩個事務,分別爲事務A和事務B,在事務A中使用 update 語句,修改 age 的值爲10,初始是1 ,在執行完 update 語句以後,在事務B中查詢 user 表,會看到 age 的值已是 10 了,這時候事務A尚未提交,而此時事務B有可能拿着已經修改過的 age=10 去進行其餘操做了。在事務B進行操做的過程當中,頗有可能事務A因爲某些緣由,進行了事務回滾操做,那其實事務B獲得的就是髒數據了,拿着髒數據去進行其餘的計算,那結果確定也是有問題的。

順着時間軸往表示兩事務中操做的執行順序,重點看圖中 age 字段的值。

讀未提交,其實就是能夠讀到其餘事務未提交的數據,但沒有辦法保證你讀到的數據最終必定是提交後的數據,若是中間發生回滾,那就會出現髒數據問題,讀未提交沒辦法解決髒數據問題。更別提可重複讀和幻讀了,想都不要想。

讀提交

既然讀未提交沒辦法解決髒數據問題,那麼就有了讀提交。讀提交就是一個事務只能讀到其餘事務已經提交過的數據,也就是其餘事務調用 commit 命令以後的數據。那髒數據問題迎刃而解了。

讀提交事務隔離級別是大多數流行數據庫的默認事務隔離界別,好比 Oracle,可是不是 MySQL 的默認隔離界別。

咱們繼續來作一下驗證,首先把事務隔離級別改成讀提交級別。

set global transaction isolation level read committed;
複製代碼

以後須要從新打開新的 session 窗口,也就是新的 shell 窗口才能夠。

一樣開啓事務A和事務B兩個事務,在事務A中使用 update 語句將 id=1 的記錄行 age 字段改成 10。此時,在事務B中使用 select 語句進行查詢,咱們發如今事務A提交以前,事務B中查詢到的記錄 age 一直是1,直到事務A提交,此時在事務B中 select 查詢,發現 age 的值已是 10 了。

這就出現了一個問題,在同一事務中(本例中的事務B),事務的不一樣時刻一樣的查詢條件,查詢出來的記錄內容是不同的,事務A的提交影響了事務B的查詢結果,這就是不可重複讀,也就是讀提交隔離級別。

每一個 select 語句都有本身的一份快照,而不是一個事務一份,因此在不一樣的時刻,查詢出來的數據多是不一致的。

讀提交解決了髒讀的問題,可是沒法作到可重複讀,也沒辦法解決幻讀。

可重複讀

可重複是對比不可重複而言的,上面說不可重複讀是指同一事物不一樣時刻讀到的數據值可能不一致。而可重複讀是指,事務不會讀到其餘事務對已有數據的修改,及時其餘事務已提交,也就是說,事務開始時讀到的已有數據是什麼,在事務提交前的任意時刻,這些數據的值都是同樣的。可是,對於其餘事務新插入的數據是能夠讀到的,這也就引起了幻讀問題。

一樣的,需改全局隔離級別爲可重複讀級別。

set global transaction isolation level repeatable read;
複製代碼

在這個隔離級別下,啓動兩個事務,兩個事務同時開啓。

首先看一下可重複讀的效果,事務A啓動後修改了數據,而且在事務B以前提交,事務B在事務開始和事務A提交以後兩個時間節點都讀取的數據相同,已經能夠看出可重複讀的效果。

可重複讀作到了,這只是針對已有行的更改操做有效,可是對於新插入的行記錄,就沒這麼幸運了,幻讀就這麼產生了。咱們看一下這個過程:

事務A開始後,執行 update 操做,將 age = 1 的記錄的 name 改成「風箏2號」;

事務B開始後,在事務執行完 update 後,執行 insert 操做,插入記錄 age =1,name = 古時的風箏,這和事務A修改的那條記錄值相同,而後提交。

事務B提交後,事務A中執行 select,查詢 age=1 的數據,這時,會發現多了一行,而且發現還有一條 name = 古時的風箏,age = 1 的記錄,這其實就是事務B剛剛插入的,這就是幻讀。

要說明的是,當你在 MySQL 中測試幻讀的時候,並不會出現上圖的結果,幻讀並無發生,MySQL 的可重複讀隔離級別其實解決了幻讀問題,這會在後面的內容說明

串行化

串行化是4種事務隔離級別中隔離效果最好的,解決了髒讀、可重複讀、幻讀的問題,可是效果最差,它將事務的執行變爲順序執行,與其餘三個隔離級別相比,它就至關於單線程,後一個事務的執行必須等待前一個事務結束。

MySQL 中是如何實現事務隔離的

首先說讀未提交,它是性能最好,也能夠說它是最野蠻的方式,由於它壓根兒就不加鎖,因此根本談不上什麼隔離效果,能夠理解爲沒有隔離。

再來講串行化。讀的時候加共享鎖,也就是其餘事務能夠併發讀,可是不能寫。寫的時候加排它鎖,其餘事務不能併發寫也不能併發讀。

最後說讀提交和可重複讀。這兩種隔離級別是比較複雜的,既要容許必定的併發,又想要兼顧的解決問題。

實現可重複讀

爲了解決不可重複讀,或者爲了實現可重複讀,MySQL 採用了 MVCC (多版本併發控制) 的方式。

咱們在數據庫表中看到的一行記錄可能實際上有多個版本,每一個版本的記錄除了有數據自己外,還要有一個表示版本的字段,記爲 row trx_id,而這個字段就是使其產生的事務的 id,事務 ID 記爲 transaction id,它在事務開始的時候向事務系統申請,按時間前後順序遞增。

按照上面這張圖理解,一行記錄如今有 3 個版本,每個版本都記錄這使其產生的事務 ID,好比事務A的transaction id 是100,那麼版本1的row trx_id 就是 100,同理版本2和版本3。

在上面介紹讀提交和可重複讀的時候都提到了一個詞,叫作快照,學名叫作一致性視圖,這也是可重複讀和不可重複讀的關鍵,可重複讀是在事務開始的時候生成一個當前事務全局性的快照,而讀提交則是每次執行語句的時候都從新生成一次快照。

對於一個快照來講,它可以讀到那些版本數據,要遵循如下規則:

  1. 當前事務內的更新,能夠讀到;
  2. 版本未提交,不能讀到;
  3. 版本已提交,可是卻在快照建立後提交的,不能讀到;
  4. 版本已提交,且是在快照建立前提交的,能夠讀到;

利用上面的規則,再返回去套用到讀提交和可重複讀的那兩張圖上就很清晰了。仍是要強調,二者主要的區別就是在快照的建立上,可重複讀僅在事務開始是建立一次,而讀提交每次執行語句的時候都要從新建立一次。

併發寫問題

存在這的狀況,兩個事務,對同一條數據作修改。最後結果應該是哪一個事務的結果呢,確定要是時間靠後的那個對不對。而且更新以前要先讀數據,這裏所說的讀和上面說到的讀不同,更新以前的讀叫作「當前讀」,老是當前版本的數據,也就是多版本中最新一次提交的那版。

假設事務A執行 update 操做, update 的時候要對所修改的行加行鎖,這個行鎖會在提交以後才釋放。而在事務A提交以前,事務B也想 update 這行數據,因而申請行鎖,可是因爲已經被事務A佔有,事務B是申請不到的,此時,事務B就會一直處於等待狀態,直到事務A提交,事務B才能繼續執行,若是事務A的時間太長,那麼事務B頗有可能出現超時異常。以下圖所示。

加鎖的過程要分有索引和無索引兩種狀況,好比下面這條語句

update user set age=11 where id = 1
複製代碼

id 是這張表的主鍵,是有索引的狀況,那麼 MySQL 直接就在索引數中找到了這行數據,而後乾淨利落的加上行鎖就能夠了。

而下面這條語句

update user set age=11 where age=10
複製代碼

表中並無爲 age 字段設置索引,因此, MySQL 沒法直接定位到這行數據。那怎麼辦呢,固然也不是加表鎖了。MySQL 會爲這張表中全部行加行鎖,沒錯,是全部行。可是呢,在加上行鎖後,MySQL 會進行一遍過濾,發現不知足的行就釋放鎖,最終只留下符合條件的行。雖然最終只爲符合條件的行加了鎖,可是這一鎖一釋放的過程對性能也是影響極大的。因此,若是是大表的話,建議合理設計索引,若是真的出現這種狀況,那很難保證併發度。

解決幻讀

上面介紹可重複讀的時候,那張圖裏標示着出現幻讀的地方實際上在 MySQL 中並不會出現,MySQL 已經在可重複讀隔離級別下解決了幻讀的問題。

前面剛說了併發寫問題的解決方式就是行鎖,而解決幻讀用的也是鎖,叫作間隙鎖,MySQL 把行鎖和間隙鎖合併在一塊兒,解決了併發寫和幻讀的問題,這個鎖叫作 Next-Key鎖。

假設如今表中有兩條記錄,而且 age 字段已經添加了索引,兩條記錄 age 的值分別爲 10 和 30。

此時,在數據庫中會爲索引維護一套B+樹,用來快速定位行記錄。B+索引樹是有序的,因此會把這張表的索引分割成幾個區間。

如圖所示,分紅了3 個區間,(負無窮,10]、(10,30]、(30,正無窮],在這3個區間是能夠加間隙鎖的。

以後,我用下面的兩個事務演示一下加鎖過程。

在事務A提交以前,事務B的插入操做只能等待,這就是間隙鎖起得做用。當事務A執行update user set name='風箏2號’ where age = 10; 的時候,因爲條件 where age = 10 ,數據庫不只在 age =10 的行上添加了行鎖,並且在這條記錄的兩邊,也就是(負無窮,10]、(10,30]這兩個區間加了間隙鎖,從而致使事務B插入操做沒法完成,只能等待事務A提交。不只插入 age = 10 的記錄須要等待事務A提交,age<十、10<age<30 的記錄頁沒法完成,而大於等於30的記錄則不受影響,這足以解決幻讀問題了。

這是有索引的狀況,若是 age 不是索引列,那麼數據庫會爲整個表加上間隙鎖。因此,若是是沒有索引的話,無論 age 是否大於等於30,都要等待事務A提交才能夠成功插入。

總結

MySQL 的 InnoDB 引擎才支持事務,其中可重複讀是默認的隔離級別。

讀未提交和串行化基本上是不須要考慮的隔離級別,前者不加鎖限制,後者至關於單線程執行,效率太差。

讀提交解決了髒讀問題,行鎖解決了併發更新的問題。而且 MySQL 在可重複讀級別解決了幻讀問題,是經過行鎖和間隙鎖的組合 Next-Key 鎖實現的。

創做不易,小小的贊,大大的暖,快來溫暖我。不用客氣了,讚我!

我是風箏,公衆號「古時的風箏」,一個在程序圈混跡多年,主業 Java,另外 Python、React 也玩兒的很 6 的斜槓開發者。能夠在公衆號中加我好友,進羣裏小夥伴交流學習,好多大廠的同窗也在羣內呦。

相關文章
相關標籤/搜索