.NET:臟讀、不可重復讀和幻讀測試


背景

昨天才發現如果一條數據被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,並不代表讀取就不采用事務了,默認的“讀已提交”還是會起作用,可以在嵌套事務中采用“讀未提交”解決這個問題。

備注

線程池和數據庫級別的鎖我還不是非常了解,有待繼續挖掘,有熟悉的朋友請給個鏈接或提示,不勝感激。

 


免責聲明!

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



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