.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,并不代表读取就不采用事务了,默认的“读已提交”还是会起作用,可以在嵌套事务中采用“读未提交”解决这个问题。
备注
线程池和数据库级别的锁我还不是非常了解,有待继续挖掘,有熟悉的朋友请给个链接或提示,不胜感激。