zoukankan      html  css  js  c++  java
  • 第五节:框架前期准备篇之锁机制处理并发

    一. 简介

    (一). 在处理并发的这个问题上,锁大致分为两类:悲观锁和乐观锁。
      1.  悲观锁:悲观的认为每次去拿数据的时候都会被别人修改,所以每次在拿数据的时候都会“上锁”,操作完成之后再“解锁”。 在数据加锁期间,其他人(其他线程)如果来拿数据就会等待,直到去掉锁。数据库层次的悲观锁有“表锁”、“行锁”等。
    注:EF默认不支持悲观锁,只能通过EF调用SQL语句。
      2.  乐观锁:乐观的认为该条数据不会被占用,自己先占了再说,占完了之后再看看是不是占上了,如果没占上就是操作失败,提示给用户。
      3.  两种锁进行对比:悲观锁使用的体验更好,但是对系统性能的影响大,只适合并发量不大的场合。 乐观锁适用于“写少读多”的情况下,加大了系统的整个吞吐量,但是“乐观锁可能失败”给用户的体验很不好。
    注:两种锁各有利弊,至于怎么取舍,根据实际业务场景来进行。
    (二). 模拟一个抢单的业务场景

      一个乘客发了一个打车订单,很多司机去抢这个订单,执行的业务简单点来说是,先select出这条数据,然后update这个条数据中的driveName字段为自己的名字,但是现在会有这么

    一种现象,同时select出这条订单,先后更新driveName这个字段,先抢到订单的乘客会发现最后订单没了,实际上是数据库中Update第二次的操作覆盖了第一次的操作了,这就是

    并发操作带来的尴尬场景。

     

    二. 悲观锁

     1. 数据准备

      新建数据库【LockDemoDB】,新建订单表OrderInfor,包括字段有:id、userName(乘客姓名)、destination(订单信息)、driverName(抢单司机的姓名)、isRobbed(该订单是否被抢, 0代表未被抢,1代表已被抢 ),事先插入一条数据用于测试对应的字段分别为: 1,  ypf,  去北京,  "",  0

    如下图:

     2. 原理

      开启事务,利用排它锁和行锁将该条数据锁住,其他线程如果要访问,必须得该线程提交完事务,锁释放后才能使用,下面分享两种写法:ADO.NET写法 和 EF调用SQL语句写法。

    大致流程:

      ①:查询id为1的数据,如果不存在,则停止业务;如果存在,继续往下执行。

      ②:查询isRobbed字段的值,如果为1,代表该订单已经刚被人抢了,然后输出driverName的值,即代表被谁抢了;如果为0,代表该订单尚未被抢,继续往下执行。

      ③:执行Update操作,进行事务提交,这期间别的线程是不能访问的。

      ④:提交完事务后,锁被释放,其它线程得以继续访问。

     ADO.NET写法:

     1             {
     2                 Console.WriteLine("司机您好,请输入您的名字");
     3                 string driverName = Console.ReadLine();
     4                 string connstr = ConfigurationManager.ConnectionStrings["connstr"].ConnectionString;
     5                 using (SqlConnection conn = new SqlConnection(connstr))
     6                 {
     7                     conn.Open();
     8                     using (var tx = conn.BeginTransaction())
     9                     {
    10                         try
    11                         {
    12                             Console.WriteLine("开始查询");
    13                             using (var selectCmd = conn.CreateCommand())
    14                             {
    15                                 selectCmd.Transaction = tx;
    16                                 //排它锁和行锁,针对访问线程锁住该行,不能继续往下执行,只有事务提交完,其他线程才能访问
    17                                 selectCmd.CommandText = "select * from OrderInfor with(xlock,ROWLOCK) where id=1";
    18                                 using (var reader = selectCmd.ExecuteReader())
    19                                 {
    20                                     if (!reader.Read())
    21                                     {
    22                                         Console.WriteLine("没有id为1的订单");
    23                                         return;
    24                                     }
    25                                     string dName = null;
    26                                     string isRobbed = null;
    27                                     if (!reader.IsDBNull(reader.GetOrdinal("driverName")))
    28                                     {
    29                                         dName = reader.GetString(reader.GetOrdinal("driverName"));
    30                                     }
    31                                     if (!reader.IsDBNull(reader.GetOrdinal("isRobbed")))
    32                                     {
    33                                         isRobbed = reader.GetString(reader.GetOrdinal("isRobbed"));
    34                                     }
    35 
    36                                     //表示该订单已经被抢了
    37                                     if (isRobbed == "1" && !string.IsNullOrEmpty(dName))
    38                                     {
    39                                         if (driverName == dName)
    40                                         {
    41                                             Console.WriteLine("该订单早已经被我抢了");
    42                                         }
    43                                         else
    44                                         {
    45                                             Console.WriteLine($"该订单早已经被司机【{dName}】抢了");
    46                                         }
    47                                         //不再往下执行
    48                                         Console.ReadKey();
    49                                         return;
    50                                     }
    51                                 }
    52                                 Console.WriteLine("查询完成,开始执行update操作");
    53                                 using (var updateCmd = conn.CreateCommand())
    54                                 {
    55                                     updateCmd.Transaction = tx;
    56                                     updateCmd.CommandText = "Update OrderInfor set driverName=@driverName,isRobbed=@isRobbed where id=1";
    57                                     updateCmd.Parameters.Add(new SqlParameter("@driverName", driverName));
    58                                     updateCmd.Parameters.Add(new SqlParameter("@isRobbed", "1"));
    59                                     updateCmd.ExecuteNonQuery();
    60                                 }
    61                                 Console.WriteLine("结束update操作");
    62                                 Console.WriteLine("按任意键进行事务提交");
    63                                 Console.ReadKey();
    64                             }
    65                             tx.Commit();
    66                             Console.WriteLine("事务提交成功");
    67                         }
    68                         catch (Exception ex)
    69                         {
    70                             Console.WriteLine(ex);
    71                             tx.Rollback();
    72                         }
    73                     }
    74                 }
    75             }

    EF调用SQL语句写法:

     1             {
     2                 Console.WriteLine("司机您好,请输入您的名字");
     3                 string driverName = Console.ReadLine();
     4                 using (LockDemoDBEntities1 ctx = new LockDemoDBEntities1())
     5                 using (var tx = ctx.Database.BeginTransaction())
     6                 {
     7                     Console.WriteLine("开始查询");
     8                     //一定要遍历一下 SqlQuery 的返回值才会真正执行 SQL 
     9                     //排它锁和行锁,针对访问线程锁住该行,不能继续往下执行,只有事务提交完,其他线程才能访问
    10                     var orderInfor = ctx.Database.SqlQuery<OrderInfor>("select * from OrderInfor with(xlock,ROWLOCK) where id=1").Single();
    11 
    12                     //表示该订单已经被抢了
    13                     if (orderInfor.isRobbed == "1" && !string.IsNullOrEmpty(orderInfor.driverName))
    14                     {
    15                         if (driverName == orderInfor.driverName)
    16                         {
    17                             Console.WriteLine("该订单早已经被我抢了");
    18                         }
    19                         else
    20                         {
    21                             Console.WriteLine($"该订单早已经被司机【{orderInfor.driverName}】抢了");
    22                         }
    23                         //不再往下执行
    24                         Console.ReadKey();
    25                         return;
    26                     }
    27 
    28                     Console.WriteLine("查询完成,开始执行update操作");
    29                     ctx.Database.ExecuteSqlCommand("Update OrderInfor set driverName={0},isRobbed={1} where id=1", driverName, "1");
    30                     Console.WriteLine("结束update操作");
    31                     Console.WriteLine("按任意键进行事务提交");
    32                     Console.ReadKey();
    33                     try
    34                     {
    35                         tx.Commit();
    36                     }
    37                     catch (Exception ex)
    38                     {
    39                         Console.WriteLine(ex);
    40                         tx.Rollback();
    41                     }
    42                 }
    43             }

     结果分析:

       ①:线程1进入,查询完毕,尚未进行事务提交。

      ②:线程2进入,被锁住,无法继续往下进行操作。

      ③:线程1进行事务提交,线程1执行成功的同时,线程2提示该订单已经被xx抢了。

     

    三. 乐观锁

     1. 数据准备

      新建订单表OrderInfor2,包括基础字段有:id、userName(乘客姓名)、destination(订单信息)、driverName(抢单司机的姓名)、isRobbed(该订单是否被抢, 0代表未被抢,1代表已被抢 ), 新增字段:rowversion字段, 类型为timestamp,对应的实体类型为byte[], 事先插入一条数据用于测试对应的字段分别为: 1, ypf, 去北京, "", 0 。

    PS:凡是对该条数据进行过update操作,rowversion字段的值都会发生变化。

     2. 原理

        这里提供两种思路,分别是:原生的SQL语句(这里通过EF调用) 和 EF默认的乐观锁模式。

    (1). 原生的SQL语句:

      ①:查出该条订单的记录,包括rowversion字段。

      ②:把该rowversion字段作为update操作where的一个条件,执行更新操作。

      ③:看受影响的行数,如果受影响的行数为0,表示该条数据在你执行更新操作前已经被人改过了,这个时候通常提示用户“更新失败”;如果受影响的行数为1,则表示没被修改过,提示用户“更新成功”。

    分享代码:

     1             {
     2                 try
     3                 {
     4                     Console.WriteLine("司机您好,请输入您的名字");
     5                     string driverName = Console.ReadLine();
     6                     using (LockDemoDBEntities1 ctx = new LockDemoDBEntities1())
     7                     {
     8                         Console.WriteLine("开始查询");
     9                         //一定要遍历一下 SqlQuery 的返回值才会真正执行 SQL 
    10                         var orderInfor = ctx.Database.SqlQuery<OrderInfor2>("select * from OrderInfor2 where id=1").Single();
    11 
    12                         //表示该订单已经被抢了
    13                         if (orderInfor.isRobbed == "1" && !string.IsNullOrEmpty(orderInfor.driverName))
    14                         {
    15                             if (driverName == orderInfor.driverName)
    16                             {
    17                                 Console.WriteLine("该订单早已经被我抢了");
    18                             }
    19                             else
    20                             {
    21                                 Console.WriteLine($"该订单早已经被司机【{orderInfor.driverName}】抢了");
    22                             }
    23                             //不在往下执行
    24                             Console.ReadKey();
    25                             return;
    26                         }
    27 
    28                         Console.WriteLine("查询完成,按任意键进行抢单");
    29                         Console.ReadKey();
    30                         Console.WriteLine("正在抢单中。。。。。");
    31                         //休眠3s,模拟高并发抢单
    32                         Thread.Sleep(3000);
    33                         int affectRows = ctx.Database.ExecuteSqlCommand("Update OrderInfor2 set driverName={0},isRobbed={1} where id=1 and rowversion={2}", driverName, "1", orderInfor.rowversion);
    34                         if (affectRows == 0)
    35                         {
    36                             Console.WriteLine("抢单失败");
    37                         }
    38                         else if (affectRows == 1)
    39                         {
    40                             Console.WriteLine("抢单成功");
    41                         }
    42                         else
    43                         {
    44                             Console.WriteLine("见鬼了");
    45                         }
    46                     }
    47                 }
    48                 catch (Exception ex)
    49                 {
    50                     Console.WriteLine("失败了");
    51                     Console.WriteLine(ex.Message);
    52                     throw;
    53                 }
    54             }

    (2). EF默认的乐观锁模式

      a. DBFirst模式:在Edmx模型上给该字段的并发模式设置为fixed(默认为None),这样该表中所有字段都监控并发。如果不想监视所有列(在不添加RowVersion的情况下),只需在Edmx模型是给特定的字段的并发模式设置为fixed,这样只有被设置的字段被监测并发。

      b. CodeFirst下的Fluent API下的配置:

          全局配置:Property(e => e.RowVersion).IsRowVersion();

          单独字段配置:Property(p => p.xxxx).IsConcurrencyToken();

      c. CodeFirst下的DataAnnotation下的配置:rowversion属性加上特性[Timestamp],这样该表中所有字段都监控并发。如果不想监视所有列(在不添加RowVersion的情况下), 只需给特定的字段加上特性 [ConcurrencyCheck],这样只有被设置的字段被监测并发。

    原理:通过DbUpdateConcurrencyException监测该条数据是否被改过,改过就抛异常。

    分享代码:

     1                 Console.WriteLine("司机您好,请输入您的名字");
     2                 string driverName = Console.ReadLine();
     3                 using (LockDemoDBEntities1 ctx = new LockDemoDBEntities1())
     4                 {
     5                     Console.WriteLine("开始查询");
     6 
     7                     var orderInfor = ctx.OrderInfor2.Where(u => u.id == "1").FirstOrDefault();
     8 
     9                     //表示该订单已经被抢了
    10                     if (orderInfor.isRobbed == "1" && !string.IsNullOrEmpty(orderInfor.driverName))
    11                     {
    12                         if (driverName == orderInfor.driverName)
    13                         {
    14                             Console.WriteLine("该订单早已经被我抢了");
    15                         }
    16                         else
    17                         {
    18                             Console.WriteLine($"该订单早已经被司机【{orderInfor.driverName}】抢了");
    19                         }
    20                         //不在往下执行
    21                         Console.ReadKey();
    22                         return;
    23                     }
    24 
    25                     Console.WriteLine("查询完成,按任意键进行抢单");
    26                     Console.ReadKey();
    27                     Console.WriteLine("正在抢单中。。。。。");
    28                     //休眠3s,模拟高并发抢单
    29                     Thread.Sleep(3000);
    30 
    31                     //下面执行更新操作
    32                     orderInfor.driverName = driverName;
    33                     orderInfor.isRobbed = "1";
    34                     try
    35                     {
    36                         ctx.SaveChanges();
    37                         Console.WriteLine("抢单成功");
    38                     }
    39                     catch (DbUpdateConcurrencyException)
    40                     {
    41                         Console.WriteLine("抢单失败");
    42                     }
    43                 }

    3. 结果分析

       ①:线程1 和 线程2,同时执行且均查询完毕,等待点击按钮进行抢单。

      ②:先点击线程1,然后点击线程2,发现线程1抢单成功,线程2抢单失败,证明线程2在抢单的时候,监测到该数据已经被改动了。

     

    四. 数据库锁详解

    详见:https://www.cnblogs.com/yaopengfei/p/9762267.html

     

    !

    • 作       者 : Yaopengfei(姚鹏飞)
    • 博客地址 : http://www.cnblogs.com/yaopengfei/
    • 声     明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
    • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
     
  • 相关阅读:
    Java day03第三课流程控制
    java day02第二课基本数据类型、修饰符、运算符
    Objective-C 枚举类型和字符串互转方案
    UISlider
    ios中AES和RSA 加密
    1. SEL和IMP(动态性)
    《iOS开发进阶》
    【旧事重提】iOS中文件读写
    Apple移动设备处理器指令集 armv6、armv7、armv7s及arm64
    Unity3D 与 Xcode的结合
  • 原文地址:https://www.cnblogs.com/yaopengfei/p/9610353.html
Copyright © 2011-2022 走看看