• 事务隔离级别如何影响锁


    目录

    备注事务隔离级别如何影响锁?更新丢失(最后一个覆盖前面的修改)使用可重复读隔离级别延长共享锁的持有时间一种避免死锁的方式备注

    备注返回目录

    我们知道事务的重要性,我们同样知道系统会出现并发,而且,一直在准求高并发,但是多数新手(包括我自己)经常忽略并发问题(更新丢失、脏读、不可重复读、幻读),如何应对并发问题呢?和线程并发控制一样,我们采用锁(乐观锁和悲观锁),大多数场景我们不需要直接管理锁,而是使用有更高语义的事务隔离级别来控制并发问题。

    关于事务、事务隔离级别如何应对并发问题的文章我之前有过介绍,可以参考如下文章:.NET:脏读、不可重复读和幻读测试

    本文重点说一下:事务隔离级别如何影响锁?

    事务隔离级别如何影响锁?返回目录

    这里大家只需知道两种锁:共享锁和排它锁,如果拿多线程相关的锁来类比的话,共享锁和排它锁共同构成了:ReadWriteLockSlim,共享锁是读取锁,排它锁是修改锁。一个资源只能拥有一个排他锁,但是可以拥有多个共享锁,而且共享锁和排它锁是互斥的,即:一个资源同时只能拥有一种锁。

    事务隔离级别不会影响排它锁,修改资源的 SQL 会给资源加排它锁,直到事务提交排它锁才会释放,如果此时有其它事务尝试读取修改的资源,读取会被挂起,因为排它锁是互斥的(使用了读未提交隔离级别的事务除外)。事务隔离级别会影响共享锁,如:是否需要共享锁?拥有共享锁多长时间?锁定多大粒度的资源?下面我们看几个具体的例子。

    更新丢失(最后一个覆盖前面的修改)返回目录

    代码

    复制代码
     1         private static void Test8()
     2         {
     3             using (var con = new SqlConnection(CONNECTION_STRING))
     4             {
     5                 con.Open();
     6                 var cmd = new SqlCommand("delete from Users", con);
     7                 cmd.ExecuteNonQuery();
     8 
     9                 cmd = new SqlCommand("insert into Users values (N'段光伟1')", con);
    10                 cmd.ExecuteNonQuery();
    11             }
    12 
    13             for (var i = 0; i < 5; i++)
    14             {
    15                 Task.Factory.StartNew((state) =>
    16                 {
    17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
    18                     {
    19                         using (var con = new SqlConnection(CONNECTION_STRING))
    20                         {
    21                             con.Open();
    22 
    23                             var index = (int)state + 1;
    24                             var padding = new String(' ', (index - 1) * 3);
    25 
    26                             Console.WriteLine(string.Format("{0}第{1}次:开始读取", padding, index));
    27                             var cmd = new SqlCommand("select top 1 * from Users", con);
    28                             cmd.ExecuteReader().Close();
    29 
    30                             Console.WriteLine(string.Format("{0}第{1}次:开始休眠", padding, index));
    31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
    32 
    33                             Console.WriteLine(string.Format("{0}第{1}次:开始修改", padding, index));
    34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
    35                             cmd.ExecuteNonQuery();
    36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
    37                         }
    38 
    39                         ts.Complete();
    40                     }
    41                 }, i).ContinueWith((t) =>
    42                 {
    43                     Console.WriteLine(t.Exception.InnerException.Message);
    44                 }, TaskContinuationOptions.OnlyOnFaulted);
    45             }
    46 
    47             Console.ReadLine();
    48         }
    复制代码

    输出

    说明

    此时会出现这种问题是因为:事务中的读取虽然会使用共享锁,但是共享锁在读取完成之后立即释放,不会等到事务提交后才释放,我们可以使用 SQL(注意看下面的 select 语句的变化) 延长共享锁的持有时间,如下:

    代码

    复制代码
     1         private static void Test9()
     2         {
     3             using (var con = new SqlConnection(CONNECTION_STRING))
     4             {
     5                 con.Open();
     6                 var cmd = new SqlCommand("delete from Users", con);
     7                 cmd.ExecuteNonQuery();
     8 
     9                 cmd = new SqlCommand("insert into Users values (N'段光伟1')", con);
    10                 cmd.ExecuteNonQuery();
    11             }
    12 
    13             for (var i = 0; i < 5; i++)
    14             {
    15                 Task.Factory.StartNew((state) =>
    16                 {
    17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
    18                     {
    19                         using (var con = new SqlConnection(CONNECTION_STRING))
    20                         {
    21                             con.Open();
    22 
    23                             var index = (int)state + 1;
    24                             var padding = new String(' ', (index - 1) * 3);
    25 
    26                             Console.WriteLine(string.Format("{0}第{1}次:开始读取", padding, index));
    27                             var cmd = new SqlCommand("select top 1 * from Users(holdlock)", con);
    28                             cmd.ExecuteReader().Close();
    29 
    30                             Console.WriteLine(string.Format("{0}第{1}次:开始休眠", padding, index));
    31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
    32 
    33                             Console.WriteLine(string.Format("{0}第{1}次:开始修改", padding, index));
    34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
    35                             cmd.ExecuteNonQuery();
    36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
    37                         }
    38 
    39                         ts.Complete();
    40                     }
    41                 }, i).ContinueWith((t) =>
    42                 {
    43                     Console.WriteLine(t.Exception.InnerException.Message);
    44                 }, TaskContinuationOptions.OnlyOnFaulted);
    45             }
    46 
    47             Console.ReadLine();
    48         }
    复制代码

    输出

    说明

    我们为 select 语句采用了 (holdlock) 后缀,这回导致共享锁直到事务提交才会释放,好无疑问,这回导致死锁,系统会选择一个胜利者,其它的都作为牺牲品。

    使用可重复读隔离级别延长共享锁的持有时间返回目录

    上面我们手工采用 SQL 来延长了共享锁的持有时间,这里演示另外一种方式。

    代码

    复制代码
     1         private static void Test10()
     2         {
     3             using (var con = new SqlConnection(CONNECTION_STRING))
     4             {
     5                 con.Open();
     6                 var cmd = new SqlCommand("delete from Users", con);
     7                 cmd.ExecuteNonQuery();
     8 
     9                 cmd = new SqlCommand("insert into Users values (N'段光伟1')", con);
    10                 cmd.ExecuteNonQuery();
    11             }
    12 
    13             for (var i = 0; i < 5; i++)
    14             {
    15                 Task.Factory.StartNew((state) =>
    16                 {
    17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.RepeatableRead }))
    18                     {
    19                         using (var con = new SqlConnection(CONNECTION_STRING))
    20                         {
    21                             con.Open();
    22 
    23                             var index = (int)state + 1;
    24                             var padding = new String(' ', (index - 1) * 3);
    25 
    26                             Console.WriteLine(string.Format("{0}第{1}次:开始读取", padding, index));
    27                             var cmd = new SqlCommand("select top 1 * from Users", con);
    28                             cmd.ExecuteReader().Close();
    29 
    30                             Console.WriteLine(string.Format("{0}第{1}次:开始休眠", padding, index));
    31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
    32 
    33                             Console.WriteLine(string.Format("{0}第{1}次:开始修改", padding, index));
    34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
    35                             cmd.ExecuteNonQuery();
    36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
    37                         }
    38 
    39                         ts.Complete();
    40                     }
    41                 }, i).ContinueWith((t) =>
    42                 {
    43                     Console.WriteLine(t.Exception.InnerException.Message);
    44                 }, TaskContinuationOptions.OnlyOnFaulted);
    45             }
    46 
    47             Console.ReadLine();
    48         }
    复制代码

    输出

    说明

    使用事务隔离级别来控制共享锁的持有时间,会影响整个事务内的所有读取。

    一种避免死锁的方式返回目录

    上面大家看到了死锁的发生,我们可以采用乐观锁 + 重试来避免这种情况,当然也可以采用另外一种数据库锁:更新锁(注意 select 语句的变化)。

    代码

    复制代码
     1         private static void Test11()
     2         {
     3             using (var con = new SqlConnection(CONNECTION_STRING))
     4             {
     5                 con.Open();
     6                 var cmd = new SqlCommand("delete from Users", con);
     7                 cmd.ExecuteNonQuery();
     8 
     9                 cmd = new SqlCommand("insert into Users values (N'段光伟1')", con);
    10                 cmd.ExecuteNonQuery();
    11             }
    12 
    13             for (var i = 0; i < 5; i++)
    14             {
    15                 Task.Factory.StartNew((state) =>
    16                 {
    17                     using (var ts = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted }))
    18                     {
    19                         using (var con = new SqlConnection(CONNECTION_STRING))
    20                         {
    21                             con.Open();
    22 
    23                             var index = (int)state + 1;
    24                             var padding = new String(' ', (index - 1) * 3);
    25 
    26                             Console.WriteLine(string.Format("{0}第{1}次:开始读取", padding, index));
    27                             var cmd = new SqlCommand("select top 1 * from Users(updlock)", con);
    28                             cmd.ExecuteReader().Close();
    29 
    30                             Console.WriteLine(string.Format("{0}第{1}次:开始休眠", padding, index));
    31                             System.Threading.Thread.Sleep(1000 * ((int)state + 1));
    32 
    33                             Console.WriteLine(string.Format("{0}第{1}次:开始修改", padding, index));
    34                             cmd = new SqlCommand("update Users set Name = N'段光宇" + state + "'", con);
    35                             cmd.ExecuteNonQuery();
    36                             Console.WriteLine(string.Format("{0}第{1}次:完成修改", padding, index));
    37                         }
    38 
    39                         ts.Complete();
    40                     }
    41                 }, i).ContinueWith((t) =>
    42                 {
    43                     Console.WriteLine(t.Exception.InnerException.Message);
    44                 }, TaskContinuationOptions.OnlyOnFaulted);
    45             }
    46 
    47             Console.ReadLine();
    48         }
    复制代码

    输出

    说明

    修改锁会阻塞其它更新锁的获取,因此所有任务都串行化了。

    备注返回目录

    最后补充一点,如果采用可串行化隔离级别,共享锁不只会延长锁定时间,锁对应的资源的粒度也会变大(锁表)。

    从此不再迷茫,感觉入门了。

     
    分类: .NET
    标签: 并发
  • 相关阅读:
    windows窗口消息内部处理机制
    iPhone and iPad Development GUI Kits, Stencils and Icons
    【转】windbg 调试经典文章(常用)
    atl和mfc
    开发IDA pro图形界面插件
    ida常用插件
    为Visual studio 2008 添加汇编工程模板
    常用软件汇总
    BOOL EnumInternetExplorer( ProcessWebBrowser pHander )
    同年龄的牛人博客
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3437968.html
Copyright © 2020-2023  润新知