.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,并不代表读取就不采用事务了,默认的“读已提交”还是会起作用,可以在嵌套事务中采用“读未提交”解决这个问题。

备注

线程池和数据库级别的锁我还不是非常了解,有待继续挖掘,有熟悉的朋友请给个链接或提示,不胜感激。

 

posted on 2013-08-27 09:23  幸福框架  阅读(4850)  评论(3编辑  收藏  举报

导航

我要啦免费统计