一. 相關概念?
? 前面系列中的章節的:?第二十二節: 以SQLServer為例介紹數據庫自有的鎖機制(共享鎖、更新鎖、排它鎖等)和事務隔離級別? 介紹了各種鎖以及事務的隔離級別,是從數據庫的角度進行介紹的,本章節是通過EF Core為載體,介紹事務隔離級別和相關問題,與上述章節有些許重復的內容。
1. 什么是事務
事務(Transaction)是由一系列對系統中數據進行訪問與更新的操作所組成的一個程序執行邏輯單元。
2. 事務的特征
事務具有 4 個基本特征,分別是:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Duration),簡稱:ACID。
(1).原子性:指事務必須是一個原子的操作序列單元。事務中包含的各項操作在一次執行過程中,只允許出現兩種狀態之一。 ? 全部執行成功 ? 全部執行失敗,任何一項操作都會導致整個事務的失敗,同時其它已經被執行的操作都將被撤銷并回滾,只有所有的操作全部成功,整個事務才算是成功完成.
(2).一致性:事務的一致性是指事務的執行不能破壞數據庫數據的完整性和一致性,一個事務在執行之前和執行之后,數據庫都必須處以一致性狀態。比如:如果從 A 賬戶轉賬到 B 賬戶,不可能因為 A 賬戶扣了錢,而 B 賬戶沒有加錢,無論 A 和 B 怎么轉賬,系統中總額是固定的,不可能因為 A 和 B 轉賬導致系統總額缺斤少兩。
(3).隔離性:指在并發環境中,并發的事務是互相隔離的,一個事務的執行不能被其它事務干擾。也就是說,不同的事務并發操作相同的數據時,每個事務都有各自完整的數據空間。一個事務內部的操作及使用的數據對其它并發事務是隔離的,并發執行的各個事務是不能互相干擾的。(詳見下面隔離級別)
(4).持久性:事務的持久性是指事務一旦提交后,數據庫中的數據必須被永久的保存下來。即使服務器系統崩潰或服務器宕機等故障。只要數據庫重新啟動,那么一定能夠將其恢復到事務成功結束后的狀態。
二. 事務隔離級別以及引發的問題
1. 隔離級別
(1).讀未提交(READ_UNCOMMITTED):讀未提交,該隔離級別允許臟讀取,其隔離級別是最低的。換句話說,如果一個事務正在處理某一數據,并對其進行了更新,但同時尚未完成事務,?因此還沒有提交事務;而以此同時,允許另一個事務也能夠訪問該數據。
【引發的問題:臟讀】
(2).讀已提交(READ_COMMITTED) :事務執行的時候只能獲取到其它事務已經提交的數據,獲取不到未提交的數據。
【解決了“臟讀”,但是解決不了“不可重復讀”】
(3).可重復讀(REPEATABLE_READ):保證在事務處理過程中,多次讀取同一個數據時,該數據的值和事務開始時刻是一致的。
【解決了“臟讀”和“不可重復度”,但是解決不了“幻讀”】
(4).順序讀(SERIALIZABLE):最嚴格的事務隔離級別。它要求所有的事務排隊順序執行,即事務只能一個接一個地處理,不能并發。
【解決上述所有情況】
注:4 種事務隔離級別從上往下,級別越高,并發性越差,安全性就越來越高。一般數據默認級別是讀已提交或可重復讀。
PS:常見數據庫的默認級別:
①:MySQL 數據庫的默認隔離級別是 Repeatable read 級別。
②:Oracle數據庫中,只支持 Seralizable 和 Read committed級別,默認的是 Read committed 級別。
③:SQL Server 數據庫中,默認的是 Read committed(讀已提交) 級別。
2.引發的問題
(1).臟讀(Dirty Read):第一個事務讀取第二個事務正在更新的數據,如果第二個事務還沒有更新完成,那么第一個事務讀取的數據將是一半為更新過的,一半還沒更新過的數據。
(2).不可重復讀(Unrepeatable Read):如果一個用戶在一個事務中多次讀取一條數據,而另外一個用戶則同時更新啦這條數據,造成第一個用戶多次讀取數據不一致。
(3).幻讀(Phantom Read):指同樣的事務操作,在前后兩個時間段內執行對同一個數據項的讀取,可能出現不一致的結果集。
3. 案例測試
(前提:初始值userAge均為1000的且id為01 和 02 兩條數據)
(1).臟讀測試:事務1兩條數據分別-500,正常事務提交后,這兩條數據的userAge的值應該均為500;將事務2設置成讀未提交(IsolationLevel.ReadUncommitted 即允許臟讀),?查出來的結果是:500,1000,即臟讀數據。
代碼分享
?


1 { 2 //1.事先準備刪除所有數據,插入兩條指定數據 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事務1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 var data1 = db.T_UserInfor.Find("01"); 19 data1.userAge -= 500; 20 db.SaveChanges(); 21 22 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 23 24 var data2 = db.T_UserInfor.Find("02"); 25 data2.userAge -= 500; 26 db.SaveChanges(); 27 28 transaction.Commit(); 29 30 } 31 catch (Exception ex) 32 { 33 34 Console.WriteLine(ex.Message); 35 } 36 } 37 } 38 }); 39 //事務2 40 Task.Run(() => 41 { 42 using (var db = new EFDB01Context()) 43 { 44 //設置成“讀未提交” 45 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) 46 { 47 try 48 { 49 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 50 var data1 = db.T_UserInfor.Find("01"); 51 var data2 = db.T_UserInfor.Find("02"); 52 53 Console.WriteLine($"01 userAge is {data1.userAge}"); 54 Console.WriteLine($"02 userAge is {data2.userAge}"); 55 56 } 57 catch (Exception ex) 58 { 59 60 Console.WriteLine(ex.Message); 61 } 62 } 63 } 64 }); 65 }
?
避免臟讀:將事務2設置成讀已提交(IsolationLevel.ReadCommitted 或者不設置,SQLServer默認就是讀已提交),則事務2需要等待事務1執行完才能讀取,讀出來的兩條數據的均為500,即避免了臟讀。
代碼分享
?


1 { 2 //1.事先準備刪除所有數據,插入兩條指定數據 3 using (EFDB01Context db = new EFDB01Context()) 4 { 5 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 6 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 8 } 9 //事務1 10 Task.Run(() => 11 { 12 using (var db = new EFDB01Context()) 13 { 14 using (var transaction = db.Database.BeginTransaction()) 15 { 16 try 17 { 18 var data1 = db.T_UserInfor.Find("01"); 19 data1.userAge -= 500; 20 db.SaveChanges(); 21 22 Task.Delay(TimeSpan.FromSeconds(10)).Wait(); 23 24 var data2 = db.T_UserInfor.Find("02"); 25 data2.userAge -= 500; 26 db.SaveChanges(); 27 28 transaction.Commit(); 29 30 } 31 catch (Exception ex) 32 { 33 34 Console.WriteLine(ex.Message); 35 } 36 } 37 } 38 }); 39 //事務2 40 Task.Run(() => 41 { 42 using (var db = new EFDB01Context()) 43 { 44 //設置成“讀已提交”,或者不設置,SQLServer默認就是讀已提交 45 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadCommitted)) 46 { 47 try 48 { 49 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 50 var data1 = db.T_UserInfor.Find("01"); 51 var data2 = db.T_UserInfor.Find("02"); 52 53 Console.WriteLine($"01 userAge is {data1.userAge}"); 54 Console.WriteLine($"02 userAge is {data2.userAge}"); 55 56 } 57 catch (Exception ex) 58 { 59 60 Console.WriteLine(ex.Message); 61 } 62 } 63 } 64 }); 65 }
?
(2).不可重復讀測試:事務1中5s后將01數據的userAge的值由1000改為500,事務2中在“讀已提交”的情況下兩次讀取的01的數據分別是1000,500,即為不可重復讀。
代碼分享
?


1 { 2 { 3 //1.事先準備刪除所有數據,插入兩條指定數據 4 using (EFDB01Context db = new EFDB01Context()) 5 { 6 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 8 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 9 } 10 //事務1 11 Task.Run(() => 12 { 13 using (var db = new EFDB01Context()) 14 { 15 using (var transaction = db.Database.BeginTransaction()) 16 { 17 try 18 { 19 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 20 21 var data1 = db.T_UserInfor.Find("01"); 22 data1.userAge -= 500; 23 db.SaveChanges(); 24 25 transaction.Commit(); 26 27 } 28 catch (Exception ex) 29 { 30 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事務2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //設置成“讀已提交”,或者不設置,SQLServer默認就是讀已提交 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.ReadCommitted)) 43 { 44 try 45 { 46 //一定要加上這句,否則下面的第二個Find不讀取數據庫 47 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 48 49 var data1 = db.T_UserInfor.Find("01"); 50 Console.WriteLine($"01 userAge is {data1.userAge}"); 51 52 Task.Delay(TimeSpan.FromSeconds(6)).Wait(); 53 54 var data2 = db.T_UserInfor.Find("01"); 55 Console.WriteLine($"01 userAge is {data2.userAge}"); 56 57 } 58 catch (Exception ex) 59 { 60 61 Console.WriteLine(ex.Message); 62 } 63 } 64 } 65 }); 66 } 67 }
?
避免不可重復讀:將事務2設置成“可重復讀”(IsolationLevel.RepeatableRead),事務2兩次讀取的數據均為1000,避免了不可重復讀。(但第二次數據和數據庫已經不一樣了,數據庫中是500)
代碼分享
?


1 { 2 { 3 //1.事先準備刪除所有數據,插入兩條指定數據 4 using (EFDB01Context db = new EFDB01Context()) 5 { 6 db.Database.ExecuteSqlCommand("truncate table T_UserInfor"); 7 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('01','ypf1','男',1000,'2019-08-08')"); 8 db.Database.ExecuteSqlCommand("insert into T_UserInfor values('02','ypf2','男',1000,'2019-08-08')"); 9 } 10 //事務1 11 Task.Run(() => 12 { 13 using (var db = new EFDB01Context()) 14 { 15 using (var transaction = db.Database.BeginTransaction()) 16 { 17 try 18 { 19 Task.Delay(TimeSpan.FromSeconds(5)).Wait(); 20 21 var data1 = db.T_UserInfor.Find("01"); 22 data1.userAge -= 500; 23 db.SaveChanges(); 24 25 transaction.Commit(); 26 27 } 28 catch (Exception ex) 29 { 30 31 Console.WriteLine(ex.Message); 32 } 33 } 34 } 35 }); 36 //事務2 37 Task.Run(() => 38 { 39 using (var db = new EFDB01Context()) 40 { 41 //設置成“可重復讀” 42 using (var transaction = db.Database.BeginTransaction(IsolationLevel.RepeatableRead)) 43 { 44 try 45 { 46 //一定要加上這句,否則下面的第二個Find不讀取數據庫 47 db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 48 49 var data1 = db.T_UserInfor.Find("01"); 50 Console.WriteLine($"01 userAge is {data1.userAge}"); 51 52 Task.Delay(TimeSpan.FromSeconds(6)).Wait(); 53 54 var data2 = db.T_UserInfor.Find("01"); 55 Console.WriteLine($"01 userAge is {data2.userAge}"); 56 57 } 58 catch (Exception ex) 59 { 60 61 Console.WriteLine(ex.Message); 62 } 63 } 64 } 65 }); 66 } 67 }
?
(3).幻讀測試
有點問題,需要在什么場景下測試??
?
三. 死鎖
?
? ?詳見開頭之前的章節,此處不再重復介紹了
?
?
!
- 作???????者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲?????明1 : 本人才疏學淺,用郭德綱的話說“我是一個小學生”,如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲?????明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。