第五節:框架前期準備篇之鎖機制處理併發

一. 簡介html

(一). 在處理併發的這個問題上,鎖大體分爲兩類:悲觀鎖和樂觀鎖。
  1.  悲觀鎖:悲觀的認爲每次去拿數據的時候都會被別人修改,因此每次在拿數據的時候都會「上鎖」,操做完成以後再「解鎖」。 在數據加鎖期間,其餘人(其餘線程)若是來拿數據就會等待,直到去掉鎖。數據庫層次的悲觀鎖有「表鎖」、「行鎖」等。
注:EF默認不支持悲觀鎖,只能經過EF調用SQL語句。
  2.  樂觀鎖:樂觀的認爲該條數據不會被佔用,本身先佔了再說,佔完了以後再看看是否是占上了,若是沒占上就是操做失敗,提示給用戶。
  3.  兩種鎖進行對比: 悲觀鎖使用的體驗更好,可是對系統性能的影響大,只適合併發量不大的場合。 樂觀鎖適用於「寫少讀多」的狀況下,加大了系統的整個吞吐量,可是「樂觀鎖可能失敗」給用戶的體驗很很差。
注:兩種鎖各有利弊,至於怎麼取捨,根據實際業務場景來進行。
(二). 模擬一個搶單的業務場景

  一個乘客發了一個打車訂單,不少司機去搶這個訂單,執行的業務簡單點來講是,先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

 

 

 

 

 

 

 

!

  • 做       者 : Yaopengfei(姚鵬飛)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 聲     明1 : 本人才疏學淺,用郭德綱的話說「我是一個小學生」,若有錯誤,歡迎討論,請勿謾罵^_^。
  • 聲     明2 : 原創博客請在轉載時保留原文連接或在文章開頭加上本人博客地址,不然保留追究法律責任的權利。
相關文章
相關標籤/搜索