FromSoftware提醒玩家 儘量別劇透《艾爾登法環》
轉載: https://zhuanlan.zhihu.com/p/117476959/
概念說明
髒讀:髒讀指的是讀到了其他事務未提交的資料,未提交意味著這些資料可能會回滾,也就是可能最終不會儲存到資料庫中, 也就是不存在的資料,讀到了並不一定最終存在的資料,這就是髒讀。
可重複讀: 可重複讀指的是在一個事務內,最開始讀到 的資料和事務結束前的任意時刻讀到的同一批資料都是一致的,通常針對資料更新(UPDATE) 操作。
不可重複讀:對比可重複讀,不可重複讀指的是在同一事務內,不同時刻讀到的同一批資料可能是不一樣的,可能會受到其他事務的影響,比如其他事務改了這批資料並提交了,通常針對資料更新(UPDATE)操作。
幻讀: 幻讀詩針對資料插入(INSERT)操作來說的。 假設事務A對某些行的內容做了更改,但是還未提交,此時事務B 插入了與事務A 更改前記錄相同的記錄行,並且在事務A提交之前提交了,而這時,在事務A中查詢,會發現好像剛剛的更改對於某些資料未起作用,但事實是事務B 剛插入進來的,讓使用者感到很魔幻,感覺出現了幻覺,這就叫幻讀。
事務隔離級別: SQL 標準定義了四種 隔離級別,MySQL 全部都支援。 這四種隔離級別分別是:
1、 讀未提交 (READ UNCOMMITTED)
2、 讀已提交 (READ COMMITTED)
3、 可重複讀 (REPEATABLE READ)
4、 序列化 (SERIALIZABLE)
從上往下,隔離強度逐漸增強,效能逐漸變差。採用哪種隔離級別要根據系統需求權衡決定,其中, 可重複讀 是MySQL 的預設級別。
事務隔離其實就是為了解決上面提到的髒讀、不可重複讀、幻讀這幾個問題, 下面展示了4種隔離級別對著三個問題的解決程度。
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
讀未提交 | 可能 | 可能 | 可能 |
讀已提交 | 不可能 | 可能 | 可能 |
可重複讀 | 不可能 | 不可能 | 可能 |
序列化 | 不可能 | 不可能 | 不可能 |
只有序列化的隔離級別解決了全部這 3個問題,其他的3個隔離級別都有缺陷。
如何設定隔離級別
# 檢視事務隔離級別 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 只針對於當前會話視窗。
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=1 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得到的就是髒資料了,拿著髒資料去進行其他的計算,那結果肯定也是有問題的。
讀提交
讀提交就是一個事午只能讀到其他事務已經提交過的資料, 也就是其他事務呼叫 commit 命令之後的資料,那髒資料問題就迎刃而解了。
讀提交事務隔離級別是大多數流行資料庫的預設事務隔離級別,比如 oracle, 但不是 MySQL 的預設隔離級別。
我們繼續來做一下驗證,首先把事務隔離級別改為讀提交級別。
set global transaction isolation level read committed;
開啟事務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 這行資料,於是申請行鎖,但是由於以ing被事務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.
+----+-----------------+------+ | id | name | age | +----+-----------------+------+ | 1 | 古時的風箏 | 10 | +----+-----------------+------+ | 2 | 風箏2號 | 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, 10<age<30 的記錄頁無法完成,而大於等於30的記錄則不受影響,這足以解決幻讀問題了。
這是有索引的情況,如果age不是索引列,那麼資料庫會為整個表加上間隙鎖。所以如果沒有索引的話,不管age是否大雨等於30, 都要等待事務A 提交才可以成功插入。
總結
MySQL 的 InnoDB 引擎才支援事務,其中可重複讀是預設的隔離級別。
讀未提交和序列化基本上是不需要考慮的隔離級別,前者不加鎖限制, 後者相當於單執行緒,效率太差。
讀提交解決了髒讀的問題,行鎖解決了併發更新的問題。 並且MySQL 在可重複讀級別解決了幻讀問題,是通過行鎖和間隙鎖的組合 Next-key 鎖實現的。