MySQL的四種事務隔離級別簡介
本文實驗的測試環境:Windows 10+cmd+MySQL+InnoDB
一、事務的基本要素(ACID)
1、原子性(Atomicity):事務開始後所有操作,要麼全部做完,要麼全部不做,不可能停滯在中間環節。事務執行過程中出錯,會回滾到事務開始前的狀態,所有的操作就像沒有發生一樣。也就是說事務是一個不可分割的整體,就像化學中學過的原子,是物質構成的基本單位。
2、一致性(Consistency):事務開始前和結束後,資料庫的完整性約束沒有被破壞 。比如A向B轉賬,不可能A扣了錢,B卻沒收到。
3、隔離性(Isolation):同一時間,只允許一個事務請求同一資料,不同的事務之間彼此沒有任何干擾。比如A正在從一張銀行卡中取錢,在A取錢的過程結束前,B不能向這張卡轉賬。
4、永續性(Durability):事務完成後,事務對資料庫的所有更新將被儲存到資料庫,不能回滾。
事務操作實踐
預設情況下,MYSQL是自動提交的,也就意味著平時我們執行一條update語句時,MYSQL是自動幫我們提交的,儘快我們沒有顯示執行commit命令。但是這種只適用於單條SQL的執行。
如果我們想要同時執行多條SQL,並且執行過程中有SQL執行異常,需要回滾前面已經成功執行的SQL或者最終想回滾全部,則必須顯示的使用事務。
1、開始一項事務:start tr ansaction或者begin;
2、提交事務:commit;
3、回滾事務:rollback;
4、事務提交之後的操作:chain;
5、事務回滾之後的操作:release;
6、修改當前連線的提交方式:set autocommit;如果設定了set autocommit=0,則設定之後所有的事務都需要顯式的通過命令來進行提交或者回滾。
二、事務的併發問題
1、髒讀:事務A讀取了事務B更新的資料,然後B回滾操作,那麼A讀取到的資料是髒資料
2、不可重複讀:事務 A 多次讀取同一資料,事務 B 在事務A多次讀取的過程中,對資料作了更新並提交,導致事務A多次讀取同一資料時,結果 不一致。
3、幻讀:系統管理員A將資料庫中所有學生的成績從具體分數改為ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。
小結:不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表
髒讀(Drity Read):某個事務已更新一份資料,另一個事務在此時讀取了同一份資料,由於某些原因,前一個RollBack了操作,則後一個事務所讀取的資料就會是不正確的。
不可重複讀(Non-repeatable read):在一個事務的兩次查詢之中資料不一致,這可能是兩次查詢過程中間插入了一個事務更新的原有的資料。
幻讀(Phantom Read):在一個事務的兩次查詢中資料筆數不一致,例如有一個事務查詢了幾列(Row)資料,而另一個事務卻在此時插入了新的幾列資料,先前的事務在接下來的查詢中,就會發現有幾列資料是它先前所沒有的。
三、MySQL事務隔離級別
事務隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
讀未提交(read-uncommitted) | 是 | 是 | 是 |
不可重複讀(read-committed) | 否 | 是 | 是 |
可重複讀(repeatable-read) | 否 | 否 | 是 |
序列化(serializable) | 否 | 否 | 否 |
mysql預設的事務隔離級別為repeatable-read
四、用例子說明各個隔離級別的情況
1、讀未提交:
(1)開啟一個客戶端A,並設定當前事務模式為read uncommitted(未提交讀),查詢表account的初始值:
(2)在客戶端A的事務提交之前,開啟另一個客戶端B,更新表account:
(3)這時,雖然客戶端B的事務還沒提交,但是客戶端A就可以查詢到B已經更新的資料:
(4)一旦客戶端B的事務因為某種原因回滾,所有的操作都將會被撤銷,那客戶端A查詢到的資料其實就是髒資料:
(5)在客戶端A執行更新語句update account set balance = balance - 50 where id =1,lilei的balance沒有變成350,居然是400,是不是很奇怪,資料不一致啊,如果你這麼想就太天真 了,在應用程式中,我們會用400-50=350,並不知道其他會話回滾了,要想解決這個問題可以採用讀已提交的隔離級別
2、讀已提交
(1)開啟一個客戶端A,並設定當前事務模式為read committed(未提交讀),查詢表account的所有記錄:
(2)在客戶端A的事務提交之前,開啟另一個客戶端B,更新表account:
(3)這時,客戶端B的事務還沒提交,客戶端A不能查詢到B已經更新的資料,解決了髒讀問題:
(4)客戶端B的事務提交
(5)客戶端A執行與上一步相同的查詢,結果 與上一步不一致,即產生了不可重複讀的問題
3、可重複讀
(1)開啟一個客戶端A,並設定當前事務模式為repeatable read,查詢表account的所有記錄
(2)在客戶端A的事務提交之前,開啟另一個客戶端B,更新表account並提交
(3)在客戶端A查詢表account的所有記錄,與步驟(1)查詢結果一致,沒有出現不可重複讀的問題
(4)在客戶端A,接著執行update balance = balance - 50 where id = 1,balance沒有變成400-50=350,lilei的balance值用的是步驟(2)中的350來算的,所以是300,資料的一致性倒是沒有被破壞。可重複讀的隔離級別下使用了MVCC機制,select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)。
(5)重新開啟客戶端B,插入一條新資料後提交
(6)在客戶端A查詢表account的所有記錄,沒有 查出 新增資料,所以沒有出現幻讀
4.序列化
(1)開啟一個客戶端A,並設定當前事務模式為serializable,查詢表account的初始值:
mysql> set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; +------+--------+---------+ | id | name | balance | +------+--------+---------+ | 1 | lilei | 10000 | | 2 | hanmei | 10000 | | 3 | lucy | 10000 | | 4 | lily | 10000 | +------+--------+---------+ 4 rows in set (0.00 sec)
(2)開啟一個客戶端B,並設定當前事務模式為serializable,插入一條記錄報錯,表被鎖了插入失敗,mysql中事務隔離級別為serializable時會鎖表,因此不會出現幻讀的情況,這種隔離級別併發性極低,開發中很少會用到。
mysql> set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into account values(5,'tom',0); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
補充:
1、事務隔離級別為讀提交時,寫資料只會鎖住相應的行
2、事務隔離級別為可重複讀時,如果檢索條件有索引(包括主鍵索引)的時候,預設加鎖方式是next-key 鎖;如果檢索條件沒有索引,更新資料時會鎖住整張表。一個間隙被事務加了鎖,其他事務是不能在這個間隙插入記錄的,這樣可以防止幻讀。
3、事務隔離級別為序列化時,讀寫資料都會鎖住整張表
4、隔離級別越高,越能保證資料的完整性和一致性,但是對併發效能的影響也越大。
然後是關於資料庫的各種鎖的總結:
1.共享鎖(又稱讀鎖)、排它鎖(又稱寫鎖):
InnoDB引擎的鎖機制:InnoDB支援事務,支援行鎖和表鎖用的比較多,Myisam不支援事務,只支援表鎖。
共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同資料集的排他鎖。
排他鎖(X):允許獲得排他鎖的事務更新資料,阻止其他事務取得相同資料集的共享讀鎖和排他寫鎖。
意向共享鎖(IS):事務打算給資料行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
意向排他鎖(IX):事務打算給資料行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
說明:
1)共享鎖和排他鎖都是行鎖,意向鎖都是表鎖,應用中我們只會使用到共享鎖和排他鎖,意向鎖是mysql內部使用的,不需要使用者干預。
2)對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及資料集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖,事務可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
**對於鎖定行記錄後需要進行更新操作的應用,應該使用Select...For update 方式,獲取排它鎖。(用共享鎖,在讀了之後再寫會阻塞,會導致死鎖)
這裡說說Myisam:MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖。
3)InnoDB行鎖是通過給索引上的索引項加鎖來實現的,因此InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索資料,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!
2.樂觀鎖、悲觀鎖:
悲觀鎖:悲觀鎖,正如其名,它指的是對資料被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)
1)使用悲觀鎖,我們必須關閉mysql資料庫的自動提交屬性,採用手動提交事務的方式,因為MySQL預設使用autocommit模式,也就是說,當你執行一個更新操作後,MySQL會立刻將結果進行提交。
2)需要注意的是,在事務中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一筆資料時會等待其它事務結束後才執行,一般SELECT ... 則不受此影響。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及資料集加排他鎖(X)。
3)補充:MySQL select…for update的Row Lock與Table Lock
使用select…for update會把資料給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB預設Row-Level Lock,所以只有「明確」地指定主鍵(或有索引的地方),MySQL 才會執行Row lock (只鎖住被選取的資料) ,否則MySQL 將會執行Table Lock (將整個資料表單給鎖住)。
樂觀鎖:
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖假設認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓返回使用者錯誤的資訊,讓使用者決定如何去做(一般是回滾事務)。那麼我們如何實現樂觀鎖呢,一般來說有以下2種方式:
1).使用資料版本(Version)記錄機制實現,這是樂觀鎖最常用的一種實現方式。何謂資料版本?即為資料增加一個版本標識,一般是通過為資料庫表增加一個數字型別的 “version” 欄位來實現。當讀取資料時,將version欄位的值一同讀出,資料每更新一次,對此version值加一。當我們提交更新的時候,判斷資料庫表對應記錄的當前版本資訊與第一次取出來的version值進行比對,如果資料庫表當前版本號與第一次取出來的version值相等,則予以更新,否則認為是過期資料。
2).樂觀鎖定的第二種實現方式和第一種差不多,同樣是在需要樂觀鎖控制的table中增加一個欄位,名稱無所謂,欄位型別使用時間戳(timestamp), 和上面的version類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。
總結:兩種鎖各有優缺點,不可認為一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了效能,所以這種情況下用悲觀鎖就比較合適。
另外,高併發情況下個人認為樂觀鎖要好於悲觀鎖,因為悲觀鎖的機制使得各個執行緒等待時間過長,極其影響效率,樂觀鎖可以在一定程度上提高併發度。
3.表鎖、行鎖
表級鎖(table-level locking):MyISAM和MEMORY儲存引擎
行級鎖(row-level locking) :InnoDB儲存引擎
頁面鎖(page-level-locking):BDB儲存引擎
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低。
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般。