背景
昨天才發現如果一條數據被A事務修改但是未提交,B事務如果采用“讀已提交”或更嚴格的隔離級別讀取改數據,會導致鎖等待,考慮到數據庫默認的隔離級別是“讀已提交”,在嵌套事務 + 子事務中有復雜的SQL查詢,很可能會出現死鎖,后面會給出嵌套事務導致死鎖的示例。
先來看看:臟讀、不可重復讀和幻讀。
臟讀
原因
當B事務在A事務修改和提交之間讀取被A事務修改的數據時,且B事務,采用了“讀未提交”隔離級別。
重現和避免
測試代碼
1 public static void 臟讀測試() 2 { 3 Console.WriteLine("\n***************重現臟讀***************。"); 4 臟讀測試(IsolationLevel.ReadUncommitted); 5 6 Console.WriteLine("\n***************避免臟讀***************。"); 7 臟讀測試(IsolationLevel.ReadCommitted); 8 } 9 10 private static void 臟讀測試(IsolationLevel readIsolationLevel) 11 { 12 var autoResetEvent = new AutoResetEvent(false); 13 var writeTransactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(120) }; 14 var readTransactionOptions = new TransactionOptions { IsolationLevel = readIsolationLevel, Timeout = TimeSpan.FromSeconds(5) }; 15 16 using (var ts1 = new TransactionScope(TransactionScopeOption.Required, writeTransactionOptions)) 17 { 18 #region 添加一條臟讀測試數據 19 20 using (var context = new TestContext()) 21 { 22 Console.WriteLine("\nA事務添加數據,未提交事務。"); 23 context.Users.AddOrUpdate(x => x.Title, new User() { Title = "臟讀測試數據" }); 24 context.SaveChanges(); 25 } 26 27 #endregion 28 29 #region 在另外一個線程讀取 30 31 ThreadPool.QueueUserWorkItem(data => 32 { 33 try 34 { 35 using (var ts3 = new TransactionScope(TransactionScopeOption.RequiresNew, readTransactionOptions)) 36 { 37 using (var context = new TestContext()) 38 { 39 Console.WriteLine("\nB事務讀取數據中..."); 40 var user = context.Users.FirstOrDefault(x => x.Title == "臟讀測試數據"); 41 Console.WriteLine("B事務讀取數據:" + user); 42 } 43 } 44 } 45 catch (Exception ex) 46 { 47 Console.WriteLine(ex.Message); 48 } 49 finally 50 { 51 autoResetEvent.Set(); 52 } 53 }); 54 55 autoResetEvent.WaitOne(); 56 autoResetEvent.Dispose(); 57 58 #endregion 59 } 60 }
輸出結果
結果分析
B事務采用“讀未提交”會出現臟讀,采用更高的隔離級別會避免臟讀。在避免中,因為還使用了線程同步,這里出現了死鎖,最終導致超時。
不可重復讀
原因
B事務在A事務的兩次讀取之間修改了A事務讀取的數據,且A事務采用了低於“可重復讀”隔離級別的事務。
重現和避免
測試代碼
1 public static void 不可重復讀測試() 2 { 3 Console.WriteLine("\n***************重現不可重復讀***************。"); 4 不可重復讀測試(IsolationLevel.ReadCommitted); 5 6 Console.WriteLine("\n***************避免不可重復讀***************。"); 7 不可重復讀測試(IsolationLevel.RepeatableRead); 8 } 9 10 private static void 不可重復讀測試(IsolationLevel readIsolationLevel) 11 { 12 //測試數據准備-開始 13 using (var context = new TestContext()) 14 { 15 context.Users.AddOrUpdate(x => x.Title, new User() { Title = "不可重復讀測試數據" }); 16 context.SaveChanges(); 17 } 18 //測試數據准備-完成 19 20 var autoResetEvent = new AutoResetEvent(false); 21 var readTransactionOptions = new TransactionOptions { IsolationLevel = readIsolationLevel, Timeout = TimeSpan.FromSeconds(120) }; 22 var writeTransactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(5) }; 23 24 using (var ts1 = new TransactionScope(TransactionScopeOption.Required, readTransactionOptions)) 25 { 26 using (var context = new TestContext()) 27 { 28 var user = context.Users.FirstOrDefault(x => x.Title.Contains("不可重復讀測試數據")); 29 Console.WriteLine("\nA事務第一次讀取:" + user.Title); 30 } 31 32 ThreadPool.QueueUserWorkItem(data => 33 { 34 try 35 { 36 using (var ts2 = new TransactionScope(TransactionScopeOption.Required, writeTransactionOptions)) 37 { 38 using (var context = new TestContext()) 39 { 40 Console.WriteLine("\nB事務中間修改,並提交事務。"); 41 var user = context.Users.FirstOrDefault(x => x.Title.Contains("不可重復讀測試數據")); 42 user.Title = user.Title + "-段光偉"; 43 context.SaveChanges(); 44 } 45 ts2.Complete(); 46 } 47 } 48 catch (Exception ex) 49 { 50 Console.WriteLine(ex.Message); 51 } 52 finally 53 { 54 autoResetEvent.Set(); 55 } 56 }); 57 58 autoResetEvent.WaitOne(); 59 autoResetEvent.Dispose(); 60 61 using (var context = new TestContext()) 62 { 63 var user = context.Users.FirstOrDefault(x => x.Title.Contains("不可重復讀測試數據")); 64 Console.WriteLine("\nA事務第二次讀取:" + user.Title); 65 } 66 } 67 68 //測試數據清理-開始 69 using (var context = new TestContext()) 70 { 71 var user = context.Users.FirstOrDefault(x => x.Title.Contains("不可重復讀測試數據")); 72 context.Users.Remove(user); 73 context.SaveChanges(); 74 } 75 //測試數據清理-完成 76 }
輸出結果
結果分析
A事務采用低於“可重復讀”隔離級別會導致“不可重復讀”,高於或等於“可重復讀”級別就可以避免這個問題。在避免中,因為還使用了線程同步,這里出現了死鎖,最終導致超時。
幻讀
原因
B事務在A事務的兩次讀取之間添加了數據,且A事務采用了低於“可序列化”隔離級別的事務。就像老師點了兩次名,人數不一樣,感覺自己出現了幻覺。
重現和避免
測試代碼
1 public static void 幻讀測試() 2 { 3 Console.WriteLine("\n***************重現幻讀***************。"); 4 幻讀測試(IsolationLevel.RepeatableRead); 5 6 Console.WriteLine("\n***************避免幻讀***************。"); 7 幻讀測試(IsolationLevel.Serializable); 8 } 9 10 private static void 幻讀測試(IsolationLevel readIsolationLevel) 11 { 12 var autoResetEvent = new AutoResetEvent(false); 13 var readTransactionOptions = new TransactionOptions { IsolationLevel = readIsolationLevel, Timeout = TimeSpan.FromSeconds(120) }; 14 var writeTransactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(5) }; 15 16 using (var ts1 = new TransactionScope(TransactionScopeOption.Required, readTransactionOptions)) 17 { 18 using (var context = new TestContext()) 19 { 20 var user = context.Users.FirstOrDefault(x => x.Title.Contains("幻讀測試數據")); 21 Console.WriteLine("\nA事務第一次讀取:" + user); 22 } 23 24 ThreadPool.QueueUserWorkItem(data => 25 { 26 try 27 { 28 using (var ts2 = new TransactionScope(TransactionScopeOption.Required, writeTransactionOptions)) 29 { 30 using (var context = new TestContext()) 31 { 32 Console.WriteLine("\nB事務中間添加,並提交事務。"); 33 context.Users.Add(new User() { Title = "幻讀測試數據" }); 34 context.SaveChanges(); 35 } 36 ts2.Complete(); 37 } 38 } 39 catch (Exception ex) 40 { 41 Console.WriteLine(ex.Message); 42 } 43 finally 44 { 45 autoResetEvent.Set(); 46 } 47 }); 48 49 autoResetEvent.WaitOne(); 50 autoResetEvent.Dispose(); 51 52 using (var context = new TestContext()) 53 { 54 var user = context.Users.FirstOrDefault(x => x.Title.Contains("幻讀測試數據")); 55 Console.WriteLine("\nA事務第二次讀取:" + user); 56 } 57 } 58 59 //測試數據清理-開始 60 using (var context = new TestContext()) 61 { 62 var user = context.Users.FirstOrDefault(x => x.Title.Contains("幻讀測試數據")); 63 if (user != null) 64 { 65 context.Users.Remove(user); 66 context.SaveChanges(); 67 } 68 } 69 //測試數據清理-完成 70 }
輸出結果
結果分析
A事務采用低於“序列化”隔離級別會導致“幻讀”,使用“序列化”級別就可以避免這個問題。在避免中,因為還使用了線程同步,這里出現了死鎖,最終導致超時。
嵌套事務導致的死鎖
測試代碼
1 public static void 嵌套事務導致的死鎖() 2 { 3 Console.WriteLine("\n***************嵌套事務導致的死鎖***************。"); 4 5 var autoResetEvent = new AutoResetEvent(false); 6 var writeTransactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TimeSpan.FromSeconds(120) }; 7 8 using (var ts1 = new TransactionScope(TransactionScopeOption.Required, writeTransactionOptions)) 9 { 10 using (var context = new TestContext()) 11 { 12 Console.WriteLine("\nA事務添加數據,未提交事務。"); 13 context.Users.AddOrUpdate(x => x.Title, new User() { Title = "臟讀測試數據" }); 14 context.SaveChanges(); 15 } 16 17 18 try 19 { 20 using (var ts2 = new TransactionScope(TransactionScopeOption.Suppress, TimeSpan.FromSeconds(5))) 21 { 22 using (var context = new TestContext()) 23 { 24 Console.WriteLine("\nA事務所在線程使用 TransactionScopeOption.Suppress 讀取數據中..."); 25 var user = context.Users.FirstOrDefault(x => x.Title == "臟讀測試數據"); 26 Console.WriteLine("A事務所在線程使用 TransactionScopeOption.Suppress 讀取數據:" + user); 27 } 28 } 29 } 30 catch (Exception ex) 31 { 32 Console.WriteLine(ex.InnerException.Message); 33 } 34 35 { 36 using (var context = new TestContext()) 37 { 38 var user = context.Users.FirstOrDefault(x => x.Title == "臟讀測試數據"); 39 Console.WriteLine("\nA事務讀取數據:" + user); 40 } 41 } 42 } 43 }
輸出結果
原因分析
雖然采用了Suppress,並不代表讀取就不采用事務了,默認的“讀已提交”還是會起作用,可以在嵌套事務中采用“讀未提交”解決這個問題。
備注
線程池和數據庫級別的鎖我還不是非常了解,有待繼續挖掘,有熟悉的朋友請給個鏈接或提示,不勝感激。