一. 簡介
可參考之前的文章: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');
案例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);
(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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。