第十節:MySQL鎖、事務隔離級別、MVCC機制詳解、間隙鎖、死鎖等


一. 簡介

可參考之前的文章:https://www.cnblogs.com/yaopengfei/p/11394728.html  (用EFCore演示了事務隔離級別)

1. 鎖定義 

 鎖是計算機協調多個進程或線程並發訪問某一資源的機制。

 在數據庫中,除了傳統的計算資源(如CPU、RAM、I/O等)的爭用以外,數據也是一種供需要用戶共享的資源。如何保證數據並發訪問的一致性、有效性是所有數據庫必須解決的一個問題,鎖沖突也是影響數據庫並發訪問性能的一個重要因素 。

2. 鎖分類

(1). 從性能上:樂觀鎖和悲觀鎖。

 A. 樂觀鎖: 通過rowversion比較數據的版本號,如果和最初數據不一致,則返回錯誤信息給用戶,讓用戶決定下一步怎么辦。

 B. 悲觀鎖:修改數據前先加鎖 鎖定,防止其他人修改。

(2). 從對數據庫操作上:讀鎖和寫鎖 (都屬於悲觀鎖)

 A. 讀鎖(共享鎖):針對同一份數據,多個讀操作可以同時進行而不會互相影響。
 B. 寫鎖(排它鎖):當前寫操作沒有完成前,它會阻斷其他寫鎖和讀鎖。

(3). 從對數據操作的顆粒上:表鎖和行鎖。

 A. 表鎖:每次操作鎖住整張表。開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。   (MyISAM存儲引擎)

 B. 行鎖:每次操作鎖住一行數據。開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度最高。(InnoDB存儲引擎,行鎖支持事務)

案例見下面的第二章節。

3. 事務

 事務是由一組SQL語句組成的邏輯處理單元,事務具有以下4個屬性,通常簡稱為事務的ACID屬性 。

 原子性(Atomicity) :事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行。

 一致性(Consistent) :在事務開始和完成時,數據都必須保持一致狀態。這意味着所有相關的數據規則都必須應用於事務的修改,以保持數據的完整性;事務結束時,所有的內部數據結構(如B樹索引或雙向鏈表)也都必須是正確的。

 隔離性(Isolation) :數據庫系統提供一定的隔離機制,保證事務在不受外部並發操作影響的“獨立”環境執行。這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然。

 持久性(Durable) 事務完成之后,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。

4. 並發事務帶來的問題 

(1). 更新丟失(Lost Update)

 當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題–最后的更新覆蓋了由其他事務所做的更新。

(2). 臟讀(Dirty Reads)

 一個事務正在對一條記錄做修改,在這個事務完成並提交前,這條記錄的數據就處於不一致的狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”數據,並據此作進一步的處理,就會產生未提交的數據依賴關系。這種現象被形象的叫做“臟讀”。

 一句話:事務A讀取到了事務B已經修改但尚未提交的數據,還在這個數據基礎上做了操作。此時,如果B事務回滾,A讀取的數據無效,不符合一致性要求。

(3). 不可重復讀(Non-Repeatable Reads)

 一個事務內,在讀取某些數據后的某個時間,再次讀取以前讀過的數據,卻發現其讀出的數據已經發生了改變、或某些記錄已經被刪除了!這種現象就叫做“不可重復讀”。

 一句話:事務A讀取到了事務B已經提交的修改數據,不符合隔離性。

(4). 幻讀(Phantom Reads)

 一個事務在未結束按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。

 一句話:事務A讀取到了事務B提交的新增數據,不符合隔離性.

總結:
  臟讀是指一個事務讀取到了其他事務沒有提交的數據,不可重復讀是指一個事務內多次根據同一個查詢條件查詢出來的“同一行記錄的值不一樣”,幻讀是指一個事務內多次 根據同個條件查出來的記錄行數不一樣。為了解決事務並發帶來的問題,才有了事務規范中的四個事務隔離級別,不同隔離級別對上面問題部分或者全部做了避免。

5. 事務隔離級別

(1).讀未提交(READ_UNCOMMITTED):讀未提交,該隔離級別允許臟讀取,其隔離級別是最低的。換句話說,如果一個事務正在處理某一數據,並對其進行了更新,但同時尚未完成事務, 因此還沒有提交事務;而以此同時,允許另一個事務也能夠訪問該數據。

 【引發的問題:臟讀】

(2).讀已提交(READ_COMMITTED) :事務執行的時候只能獲取到其它事務已經提交的數據,獲取不到未提交的數據。

 【解決了“臟讀”,但是解決不了“不可重復讀”】

(3).可重復讀(REPEATABLE_READ):保證在事務處理過程中,多次讀取同一個數據時,該數據的值和事務開始時刻是一致的。

 【解決了“臟讀”和“不可重復度”,但是解決不了“幻讀”】

(4).順序讀(可串行化)(SERIALIZABLE):最嚴格的事務隔離級別。它要求所有的事務排隊順序執行,即事務只能一個接一個地處理,不能並發。

 【解決上述所有情況】

 

注:4 種事務隔離級別從上往下,級別越高,並發性越差,安全性就越來越高。一般數據默認級別是讀已提交或可重復讀。

PS:常見數據庫的默認級別:

 ①:MySQL 數據庫的默認隔離級別是 REPEATABLE_READ(可重復讀) 級別。所以mysql中不會出現臟讀、不可重復讀,但是會出現幻讀。

 ②:Oracle數據庫中,只支持 SERIALIZABLE 和 READ_COMMITTED級別,默認的是 READ_COMMITTED 級別。

 ③:SQL Server 數據庫中,默認的是 READ_COMMITTED(讀已提交) 級別。

PS:MySQL下的指令:

 ①:查看事務隔離級別

show variables like 'tx_isolation';

 ②:設置事務隔離級別(僅僅針對當前會話有效)

set tx_isolation='REPEATABLE-READ';

 

二. 實戰

 1. 讀鎖和寫鎖

 表和數據准備(這里使用MyISAM引擎下的表級別鎖)

‐‐建表SQL
CREATE TABLE `mylock` (
`id` INT (11) NOT NULL AUTO_INCREMENT,
`NAME` VARCHAR (20) DEFAULT NULL,
 PRIMARY KEY (`id`)
 ) ENGINE = MyISAM DEFAULT CHARSET = utf8;
‐‐插入數據
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO  `mylock` (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO  `mylock` (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO   `mylock` (`id`, `NAME`) VALUES ('4', 'd');
View Code

案例1-讀鎖

(1). 在會話1中給mylock表添加讀鎖

 lock table mylock read;

(2). 分別在會話1和會話2中進行數據查詢,均可查詢成功。

 

(3). 分別在會話1和會話2中進行插入或更新操作,會話1報錯,會話2等待。

(4). 釋放會話1的鎖【unlock tables;】,會話2插入成功。

 

案例2-寫鎖

 添加寫鎖的指令:

--寫鎖
lock table mylock write;

 重復上述步驟,得出的結論:當前會話對該表的增刪改查都沒有問題,其他會話對該表的所有操作被阻塞。

總結:

MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行增刪改操作前,會自動給涉及的表加寫鎖。

 (1). 對MyISAM表的讀操作(加讀鎖) ,不會阻寒其他進程對同一表的讀請求,但會阻塞對同一表的寫請求。只有當讀鎖釋放后,才會執行其它進程的寫操作。

 (2). 對MylSAM表的寫操作(加寫鎖) ,不會祖冊當前進程,但會阻塞其他進程對同一表的讀和寫操作,只有當寫鎖釋放后,才會執行其它進程的讀寫操作

簡而言之,就是讀鎖會阻塞寫,但是不會阻塞讀。而寫鎖則會把讀和寫都阻塞。

補充查看鎖的指令:

--查看鎖(In_use為1代表有鎖)
show open tables;

2. 讀未提交

數據准備(后面案例都基於這些數據)

--創建表
 CREATE TABLE `account` (
`id` INT (11) NOT NULL AUTO_INCREMENT,
`name` VARCHAR (255) DEFAULT NULL,
 `balance` int(255) NULL DEFAULT NULL,
 PRIMARY KEY (`id`)
 ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
--清空表數據
truncate table `account`;
--插入數據
INSERT INTO `IndexTestDB`.`account`(`name`, `balance`) VALUES ('lilei', 450);
INSERT INTO `IndexTestDB`.`account`(`name`, `balance`) VALUES ('hanmei', 16000);
INSERT INTO `IndexTestDB`.`account`(`name`, `balance`) VALUES ('lucy', 2400);
View Code

(1). 設置會話A的事務隔離級別為【讀未提交】 

set session transaction isolation level read uncommitted;

開啟事務,進行數據查詢,如下圖:

select * from account;

 

 (2). 打開會話B,保持默認的事務隔離級別即可,開啟事務,執行一條更新操作,不提交事務

update account set balance=balance-50 where id=1;

(3). 回到會話A,再次查詢數據,發現讀取到了事務B修改后的數據,但是事務B並沒有提交,這就是臟讀

剖析:在實際應用程序中,我們會拿着400去做業務處理,即使會話B事務回滾了,應用程序也不知道。

3. 讀已提交

(1). 設置會話A的事務隔離級別為【讀已提交】 ,開啟事務,進行數據查詢。

set session transaction isolation level read committed;

 

(2).  打開會話B,保持默認的事務隔離級別即可,開啟事務,執行一條更新操作,不提交事務

(3). 回到會話A,進行數據查詢,發現查詢的結果是450,而不是400,說明會話A並沒有讀取會話B事務沒有提交的數據,解決了臟讀問題。 

 

(4). 回到會話B,進行事務提交。

 

(5). 回到會話A,繼續進行數據查詢,發現查出來的數據是400,在同一個事務中,事務還沒有結束,同樣的語句查出來不同的結果值,這就是不可重復讀

 

4. 可重復讀

 (1). 設置會話A的事務隔離級別為【讀已提交】 ,開啟事務,進行數據查詢。(或者不做任何設置,mysql的默認事務隔離級別就是讀已提交)

set session transaction isolation level repeatable read;

 

 (2). 打開會話B,開啟事務,更新數據,並且事務提交。

(3). 回到會話A,進行數據查詢,發現查出來的結果是450,兩次查詢結果一致,解決了不可重復讀問題。 

(4). 在會話A中執行更新操作,發現結果是350,而不是400,數據的一致性倒是沒有被破壞,這就是MVCC機制

可重復讀的隔離級別下使用了MVCC(multi-version concurrency control)機制,select操作不會更新版本號,是快照讀(歷史版本);insert、update和delete會更新版本號,是當前讀(當前版本)。

 

下面開始驗證幻讀:

(5). 重新打開會話B,開啟事務,插入一條id=3的數據,提交事務。

(6). 回到會話A,進行數據查詢,仍然是兩條數據,並沒有出現幻讀

(7). 在會話A中執行對 id=3的數據的更新操作。然后進行數據查詢,出現了3條數據,即多了一條id=3的數據,這就是幻讀

 注:上述6中查不出來數據,7中更新一下查出來數據,結合下面的MVCC詳解來理解這個現象。

5.  串行化

(1). 設置會話A的事務隔離級別為【串行化】 ,開啟事務,進行數據查詢。

set session transaction isolation level serializable;

(2). 打開會話B,設置隔離級別為【串行化】,開啟事務,進行數據插入操作,發現並不能插入,一直在等待,從而解決了幻讀問題。(這種級別非常少用)

6. 行鎖

 會話A開啟事務更新id=1的數據,不提交;會話B開啟事務,更新id=1的數據會被阻塞,更新其他數據沒問題。

PS:以前理解的臟讀是不准確的,那只是並發導致的一個bug,可以認為他是更新丟失。

 

三. MVCC機制

1. 業務准備

 開啟3個事務操作accout表,然后開啟兩個事務進行select查詢操作,如下圖:

 

2. MVCC底層剖析

(1). 底部版本鏈記錄 和 原理分析

(2). 分析查詢事務Select1 

 A. 步驟(5),未提交的事務id為100和200,已經提交的事務id為300,所以生成的readview為 [100,200],300,從上往下分析版本鏈,

  ① 步驟(3)  trx_id=300,300屬於右上圖橙色范疇,且不在trx_id 的數組中,所以表示該行記錄是已經提交的的事務生成的,數據是可見的,所以name=ypf300.

 

 

 B. 步驟(8),同一個事務中readview是不變的,所以 readview為 [100,200],300,從上往下分析版本鏈:

  ① 步驟(7), trx_id=100,100在readview的數組中,表示該行記錄是由還沒有提交的事務生成的,所以不可見。

  ② 步驟(6), 同步驟(7), 數據不可見

  ③ 步驟(3),  上面的A中已經分析過,數據可見,所以name=ypf300

 C. 步驟(12), 同一個事務中readview是不變的,所以 readview為 [100,200],300,從上往下分析版本鏈:

  ① 步驟(11), trx_id=200,200在readview的數組中,表示該行記錄是由還沒有提交的事務生成的,所以不可見。

  ② 步驟(10), 同步驟(11), 數據不可見

  ① 步驟(7), trx_id=100,100在readview的數組中,表示該行記錄是由還沒有提交的事務生成的,所以不可見。

  ② 步驟(6), 同步驟(7), 數據不可見

  ③ 步驟(3),  上面的A中已經分析過,數據可見,所以name=ypf300

 

(3). 分析查詢事務Select12

 步驟(13),未提交的事務id為200,已經提交的事務id為300,所以生成的readview為 [200],300,從上往下分析版本鏈,

  ① 步驟(11), trx_id=200, 200在readview的數組中,表示該行記錄是由還沒有提交的事務生成的,所以不可見。

  ② 步驟(10), 同步驟(11), 數據不可見

  ① 步驟(7), trx_id=100,100<200,屬於右上圖綠色范疇,表示該行記錄是由已經提交的事務生成的,是可見的,所有name=ypf2。

 

 

四. 其它補充

1. 間隙鎖

 間隙鎖在某些情況下可以解決幻讀問題。

 要避免幻讀可以用間隙鎖在會話A下面執行,則其他會話沒法在這個范圍所包含的間隙里插入或修改任何數據。

update account set name ='zhuge' where id > 10 and id <=20;,

2. 行鎖升級表鎖

(1). 鎖主要是加在索引上,如果對非索引字段更新, 行鎖可能會變表鎖

會話A執行:

update account set balance = 800 where name = 'lilei';

(2). 會話B對該表任一行操作都會阻塞住

 InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖。並且該索引不能失效,否則都會從行鎖升級為表鎖。

鎖定某一行還可以用lock in share mode(共享鎖) 和for update(排它鎖),例如:select * from test_innodb_lock where a = 2 for update; 這樣其他session只能讀這行數據,修改則會被阻塞,直到鎖定行的session提交。

補充:行鎖分析

show status like'innodb_row_lock%';

對各個狀態量的說明如下:

 Innodb_row_lock_current_waits: 當前正在等待鎖定的數量

 Innodb_row_lock_time: 從系統啟動到現在鎖定總時間長度

 Innodb_row_lock_time_avg: 每次等待所花平均時間

 Innodb_row_lock_time_max:從系統啟動到現在等待最長的一次所花時間

 Innodb_row_lock_waits:系統啟動后到現在總共等待的次數

對於這5個狀態變量,比較重要的主要是:

 Innodb_row_lock_time_avg (等待平均時長)

 Innodb_row_lock_waits (等待總次數)

 Innodb_row_lock_time(等待總時長)

尤其是當等待次數很高,而且每次等待時長也不小的時候,我們就需要分析系統中為什么會有如此多的等待,然后根據分析結果着手制定優化計划。

3. 死鎖 

會話A執行:
select * from account where id=1 for update;
會話B執行:
select * from account where id=2 for update;
會話A執行:
select * from account where id=2 for update;
會話B執行:
select * from account where id=1 for update;

PS:大多數情況mysql可以自動檢測死鎖並回滾產生死鎖的那個事務,但是有些情況mysql沒法自動檢測死鎖

4. 優化建議

 (1). 盡可能讓所有數據檢索都通過索引來完成,避免無索引行鎖升級為表鎖

 (2). 合理設計索引,盡量縮小鎖的范圍

 (3). 盡可能減少檢索條件范圍,避免間隙鎖

 (4). 盡量控制事務大小,減少鎖定資源量和時間長度,涉及事務加鎖的sql

 (5). 盡量放在事務最后執行

 (6). 盡可能低級別事務隔離

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM