目录

背景脏读原因重现和避免不可重复读原因重现和避免幻读原因重现和避免嵌套事务导致的死锁备注

背景返回目录

昨天才发现如果一条数据被A事务修改但是未提交,B事务如果采用“读已提交”或更严格的隔离级别读取改数据,会导致锁等待,考虑到数据库默认的隔离级别是“读已提交”,在嵌套事务 + 子事务中有复杂的SQL查询,很可能会出现死锁,后面会给出嵌套事务导致死锁的示例。

先来看看:脏读、不可重复读和幻读。

脏读返回目录

原因返回目录

当B事务在A事务修改和提交之间读取被A事务修改的数据时,且B事务,采用了“读未提交”隔离级别。

.NET:脏读、不可重复读和幻读测试-LMLPHP

重现和避免返回目录

测试代码

.NET:脏读、不可重复读和幻读测试-LMLPHP
 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 }
.NET:脏读、不可重复读和幻读测试-LMLPHP

输出结果

.NET:脏读、不可重复读和幻读测试-LMLPHP

结果分析

B事务采用“读未提交”会出现脏读,采用更高的隔离级别会避免脏读。在避免中,因为还使用了线程同步,这里出现了死锁,最终导致超时。

不可重复读返回目录

原因返回目录

B事务在A事务的两次读取之间修改了A事务读取的数据,且A事务采用了低于“可重复读”隔离级别的事务。

.NET:脏读、不可重复读和幻读测试-LMLPHP

重现和避免返回目录

测试代码

.NET:脏读、不可重复读和幻读测试-LMLPHP
 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 }
.NET:脏读、不可重复读和幻读测试-LMLPHP

输出结果

.NET:脏读、不可重复读和幻读测试-LMLPHP

结果分析

A事务采用低于“可重复读”隔离级别会导致“不可重复读”,高于或等于“可重复读”级别就可以避免这个问题。在避免中,因为还使用了线程同步,这里出现了死锁,最终导致超时。

幻读返回目录

原因返回目录

B事务在A事务的两次读取之间添加了数据,且A事务采用了低于“可序列化”隔离级别的事务。就像老师点了两次名,人数不一样,感觉自己出现了幻觉。

.NET:脏读、不可重复读和幻读测试-LMLPHP

重现和避免返回目录

测试代码

.NET:脏读、不可重复读和幻读测试-LMLPHP
 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 }
.NET:脏读、不可重复读和幻读测试-LMLPHP

输出结果

.NET:脏读、不可重复读和幻读测试-LMLPHP

结果分析

A事务采用低于“序列化”隔离级别会导致“幻读”,使用“序列化”级别就可以避免这个问题。在避免中,因为还使用了线程同步,这里出现了死锁,最终导致超时。

嵌套事务导致的死锁返回目录

测试代码

.NET:脏读、不可重复读和幻读测试-LMLPHP
 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 }
.NET:脏读、不可重复读和幻读测试-LMLPHP

输出结果

.NET:脏读、不可重复读和幻读测试-LMLPHP

原因分析

虽然采用了Suppress,并不代表读取就不采用事务了,默认的“读已提交”还是会起作用,可以在嵌套事务中采用“读未提交”解决这个问题。

备注返回目录

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

05-11 13:49