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


    背景

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

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

    脏读

    原因

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

    重现和避免

    测试代码

     1         public static void 脏读测试()
     2         {
     3             Console.WriteLine("
    ***************重现脏读***************。");
     4             脏读测试(IsolationLevel.ReadUncommitted);
     5 
     6             Console.WriteLine("
    ***************避免脏读***************。");
     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("
    A事务添加数据,未提交事务。");
    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("
    B事务读取数据中...");
    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("
    ***************重现不可重复读***************。");
     4             不可重复读测试(IsolationLevel.ReadCommitted);
     5 
     6             Console.WriteLine("
    ***************避免不可重复读***************。");
     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("
    A事务第一次读取:" + 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("
    B事务中间修改,并提交事务。");
    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("
    A事务第二次读取:" + 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("
    ***************重现幻读***************。");
     4             幻读测试(IsolationLevel.RepeatableRead);
     5 
     6             Console.WriteLine("
    ***************避免幻读***************。");
     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("
    A事务第一次读取:" + 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("
    B事务中间添加,并提交事务。");
    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("
    A事务第二次读取:" + 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("
    ***************嵌套事务导致的死锁***************。");
     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("
    A事务添加数据,未提交事务。");
    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("
    A事务所在线程使用 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("
    A事务读取数据:" + user);
    40                     }
    41                 }
    42             }
    43         }

    输出结果

    原因分析

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

    备注

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

  • 相关阅读:
    ADO.NET FOR MySQL帮助类
    遍历文件夹及其子文件夹下的.pdf文件,并解压文件夹下所有的压缩包
    history.back();谷歌浏览器,iframe后退问题
    Request.url请求属性
    正则表达式 取两字符中间的字符串(双向非贪婪模式)
    MVC 项目中为什么会有两个web.config
    c#动态编译并动态生成dll
    siteserver cms选择栏目搜索无效
    jquery修改Switchery复选框的状态
    ntko office在线编辑控件问题记录
  • 原文地址:https://www.cnblogs.com/happyframework/p/3284154.html
Copyright © 2020-2023  润新知