一. 简介html
一个乘客发了一个打车订单,不少司机去抢这个订单,执行的业务简单点来讲是,先select出这条数据,而后update这个条数据中的driveName字段为本身的名字,可是如今会有这么数据库
一种现象,同时select出这条订单,前后更新driveName这个字段,先抢到订单的乘客会发现最后订单没了,其实是数据库中Update第二次的操做覆盖了第一次的操做了,这就是并发
并发操做带来的尴尬场景。高并发
二. 悲观锁性能
1. 数据准备测试
新建数据库【LockDemoDB】,新建订单表OrderInfor,包括字段有:id、userName(乘客姓名)、destination(订单信息)、driverName(抢单司机的姓名)、isRobbed(该订单是否被抢, 0表明未被抢,1表明已被抢 ),事先插入一条数据用于测试对应的字段分别为: 1, ypf, 去北京, "", 0spa
以下图:线程
2. 原理code
开启事务,利用排它锁和行锁将该条数据锁住,其余线程若是要访问,必须得该线程提交完事务,锁释放后才能使用,下面分享两种写法:ADO.NET写法 和 EF调用SQL语句写法。htm
大体流程:
①:查询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在抢单的时候,监测到该数据已经被改动了。
四. 数据库锁详解
详见:http://www.javashuo.com/article/p-fkgnoegt-hh.html
!