標籤:row 串列 使用者 項目 並發控制 png 使用者體驗 monitor 簡化
還是那句老話:十年河東,十年河西,莫欺騷年窮!~_~ 打錯個字,應該是莫欺少年窮!
學曆代表你的過去,能力代表你的現在,學習代表你的將來。
學無止境,精益求精。
自ASP.NET誕生以來,微軟提供了不少控制並發的方法,在瞭解這些控制並發的方法前,我們先來簡單介紹下並發!
並發:同一時間或者同一時刻多個訪問者同時訪問某一更新操作時,會產生並發!
針對並發的處理,又分為封閉式並行存取處理和開放式並行存取處理
所謂悲觀/開放式並行存取處理,可以這樣理解:
悲觀者認為:在程式的運行過程中,並發很容易發生滴,因此,悲觀者提出了他們的處理模式:在我執行一個方法時,不允許其他訪問者介入這個方法。(悲觀者經常認為某件壞事會發生在自己身上)
樂觀者認為:在程式的運行過程中,並發是很少發生滴,因此,樂觀者提出了他們的處理模式:在我執行一個方法時,允許其他訪問者介入這個方法。(樂觀者經常認為某件壞事不會發生在自己身上)
那麼在C#語言中,那些屬於悲觀者呢?
在C#中諸如:LOCK、Monitor、Interlocked 等鎖定資料的方式,屬於封閉式並行存取處理範疇!資料一旦被鎖定,其他訪問者均無權訪問。有興趣的可以參考:鎖、C#中Monitor和Lock以及區別
但是,悲觀者處理並發的模式有一個通病,那就是可能會造成非常低下的執行效率。
在此:舉個簡單例子:
售票系統,小明去買票,要買北京到上海的D110次列車,如果採用悲觀者處理並發的模式,那麼售票員會將D110次列車的票鎖定,然後再作出票操作。但是,在D110次列車車票被鎖定期間,售票員去了趟廁所,或者喝了杯咖啡,其他視窗售票員是不能進行售票滴!如果採用這種處理方式的話,中國14億人口都不用出行了,原因是買不到票 ~_~
因此:在處理資料庫並發時,悲觀鎖還是要謹慎使用!具體還要看資料庫並發量大不大,如果比較大,建議使用樂觀者處理模式,如果比較小,可以適當採用悲觀者處理模式!
OK。說了這麼多,也就是做個鋪墊,本節內容標題叫資料庫並發的解決方案,我們最終還得返璞歸真,從資料庫並發的解決說起!
那麼問題來了?
資料庫並發的處理方式有哪些呢?
其實資料庫的並發處理也是分為樂觀鎖和悲觀鎖,只不過是基於資料庫層面而言的!關於資料庫層面的並發處理大家可參考我的部落格:樂觀鎖悲觀鎖應用
悲觀鎖:假定會發生並發衝突,屏蔽一切可能違反資料完整性的操作。[1]
樂觀鎖:假設不會發生並發衝突,只在提交操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。
最常用的處理多使用者並發訪問的方法是加鎖。當一個使用者鎖住資料庫中的某個對象時,其他使用者就不能再訪問該對象。加鎖對並發訪問的影響體現在鎖的粒度上。比如,放在一個表上的鎖限制對整個表的並發訪問;放在資料頁上的鎖限制了對整個資料頁的訪問;放在行上的鎖只限制對該行的並發訪問。可見行鎖粒度最小,並發訪問最好,頁鎖粒度最大,並發訪問效能就會越低。
悲觀鎖:假定會發生並發衝突,屏蔽一切可能違反資料完整性的操作。[1] 悲觀鎖假定其他使用者企圖訪問或者改變你正在訪問、更改的對象的機率是很高的,因此在悲觀鎖的環境中,在你開始改變此對象之前就將該對象鎖住,並且直到你提交了所作的更改之後才釋放鎖。悲觀的缺陷是不論是頁鎖還是行鎖,加鎖的時間可能會很長,這樣可能會長時間的鎖定一個對象,限制其他使用者的訪問,也就是說悲觀鎖的並發訪問性不好。
樂觀鎖:假設不會發生並發衝突,只在提交操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。 樂觀鎖則認為其他使用者企圖改變你正在更改的對象的機率是很小的,因此樂觀鎖直到你準備提交所作的更改時才將對象鎖住,當你讀取以及改變該對象時並不加鎖。可見樂觀鎖加鎖的時間要比悲觀鎖短,樂觀鎖可以用較大的鎖粒度獲得較好的並發訪問效能。但是如果第二個使用者恰好在第一個使用者提交更改之前讀取了該對象,那麼當他完成了自己的更改進行提交時,資料庫就會發現該對象已經變化了,這樣,第二個使用者不得不重新讀取該對象並作出更改。這說明在樂觀鎖環境中,會增加並發使用者讀取對象的次數。
本篇的主旨是講解基於C#的資料庫並發解決方案(通用版、EF版),因此我們要從C#方面入手,最好是結合一個小項目
項目已為大家準備好了,如下:
首先我們需要建立一個小型資料庫:
View Code
建立的資料庫很簡單,三張表:商品表,庫存表,日誌表
有了資料庫,我們就建立C#項目,本項目採用C# DataBaseFirst 模式,結構如下:
項目很簡單,採用EF DataBaseFirst 模式很好構建。
項目構建好了,下面我們類比並發的發生?
主要代碼如下(減少庫存、插入日誌):
#region 未做並發處理 /// <summary> /// 模仿一個減少庫存操作 不加並發控制 /// </summary> public void SubMitOrder_3() { int productId = 1; using (BingFaTestEntities context = new BingFaTestEntities()) { var InventoryLogDbSet = context.InventoryLog; var InventoryDbSet = context.Inventory;//庫存表 using (var Transaction = context.Database.BeginTransaction()) { //減少庫存操作 var Inventory_Mol = InventoryDbSet.Where(A => A.ProductId == productId).FirstOrDefault();//庫存對象 Inventory_Mol.ProductCount = Inventory_Mol.ProductCount - 1; int A4 = context.SaveChanges(); //插入日誌 InventoryLog LogModel = new InventoryLog() { Title = "插入一條資料,用於計算是否發生並發", }; InventoryLogDbSet.Add(LogModel); context.SaveChanges(); //1.5 類比耗時 Thread.Sleep(500); //消耗半秒鐘 Transaction.Commit(); } } } #endregion
此時我們 int productId=1 處加上斷點,並運行程式(開啟四個瀏覽器同時執行),如下:
由可知,四個訪問者同時訪問這個未採用並發控制的方法,得到的結果如下:
結果顯示:日誌產生四條資料,而庫存量缺只減少1個。這個結果顯然是不正確的,原因是因為發生了並發,其本質原因是髒讀,誤讀,不可重讀造成的。
那麼,問題既然發生了,我們就想辦法法解決,辦法有兩種,分別為:悲觀鎖方法、樂觀鎖方法。
悲觀者方法:
悲觀者方法(加了uodlock鎖,鎖定了更新操作,也就是說,一旦被鎖定,其他訪問者不允許訪問此操作)類似這種方法,可以通過預存程序實現,在此不作解釋了
樂觀者方法(通用版/預存程序實現):
在上述資料庫指令碼中,有欄位叫做:VersionNum,類型為:TimeStamp。
欄位 VersionNum 大家可以理解為版本號碼,版本號碼的作用是一旦有訪問者修改資料,版本號碼的值就會相應發生改變。當然,版本號碼的同步更改是和資料庫相關的,在SQLserver中會隨著資料的修改同步更新版本號碼,但是在MySQL裡就不會隨著資料的修改而更改。因此,如果你採用的是MYSQL資料庫,就需要寫一個觸發器,如下:
OK,瞭解了類型為Timestamp的欄位,下面我們結合上述的小型資料庫建立一個處理並發的預存程序,如下
create proc LockProc --樂觀鎖控制並發(@ProductId int, @IsSuccess bit=0 output)asdeclare @count as intdeclare @flag as TimeStampdeclare @rowcount As int begin transelect @count=ProductCount,@flag=VersionNum from Inventory where [email protected] update Inventory set [email protected] where [email protected] and [email protected]insert into InventoryLog values(‘插入一條資料,用於計算是否發生並發‘)set @[email protected]@ROWCOUNTif @rowcount>0set @IsSuccess=1elseset @IsSuccess=0commit tran
這個預存程序很簡單,執行兩個操作:減少庫存和插入一條資料。有一個輸入參數:productId ,一個輸出參數,IsSuccess。如果發生並發,IsSuccess的值為False,如果執行成功,IsSuccess值為True。
在這裡,向大家說明一點:程式採用悲觀鎖,是串列的,採用樂觀鎖,是並行的。
也就是說:採用悲觀鎖,一次僅執行一個訪問者的請求,待前一個訪問者訪問完成並釋放鎖時,下一個訪問者會依次進入鎖定的程式並執行,直到所有訪問者執行結束。因此,悲觀鎖嚴格按照次序執行的模式能保證所有訪問者執行成功。
採用樂觀鎖時,訪問者是並存執行的,大家同時訪問一個方法,只不過同一時刻只會有一個訪問者操作成功,其他訪問者執行失敗。那麼,針對這些執行失敗的訪問者怎麼處理呢?直接返回失敗資訊是不合理的,使用者體驗不好,因此,需要定製一個規則,讓執行失敗的訪問者重新執行之前的請求即可。
時間有限,就不多寫了...因為並發的控制是在資料庫端預存程序,所以,C#代碼也很簡單。如下:
View Code
在此,需要說明如下:
當IsSuccess的值為False時,應該重複執行該方法,我定的規則是重複請求十次,這樣就很好的解決了直接反饋給使用者失敗的訊息。提高了使用者體驗。
下面著重說下EF架構如何避免資料庫並發,在講解之前,先允許我引用下別人部落格中的幾段話:
在軟體開發過程中,並發控制是確保及時糾正由並行作業導致的錯誤的一種機制。從 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NET Entity Framework,.NET 都為並發控制提供好良好的支援方案。
相對於資料庫中的並發處理方式,Entity Framework 中的並發處理方式實現了不少的簡化。
在System.Data.Metadata.Edm 命名空間中,存在ConcurencyMode 枚舉,用於指定概念性模型中的屬性的並發選項。
ConcurencyMode 有兩個成員:
| 成員名稱 |
說明 |
| None |
在寫入時從不驗證此屬性。 這是預設的併發模式。 |
| Fixed |
在寫入時始終驗證此屬性。 |
當模型屬性為預設值 None 時,系統不會對此模型屬性進行檢測,當同一個時間對此屬性進行修改時,系統會以資料合併方式處理輸入的屬性值。
當模型屬性為Fixed 時,系統會對此模型屬性進行檢測,當同一個時間對屬性進行修改時,系統就會激發OptimisticConcurrencyException 異常。
開發人員可以為對象的每個屬性定義不同的 ConcurencyMode 選項,選項可以在*.Edmx找看到:
Edmx檔案用記事本開啟如下:
View Code
其實,在EF DataBaseFirst中,我們只需設定下類型為 TimeStamp 版本號碼的屬性即可,如下:
設定好了版本號碼屬性後,你就可以進行並發測試了,當系統發生並發時,程式會拋出異常,而我們要做的就是要捕獲這個異常,而後就是按照自己的規則,重複執行請求的方法,直至返回成功為止。
那麼如何捕獲並發異常呢?
在C#代碼中需要使用異常類:DbUpdateConcurrencyException 來捕獲,EF中具體用法如下:
public class SaveChangesForBF : BingFaTestEntities { public override int SaveChanges() { try { return base.SaveChanges(); } catch (DbUpdateConcurrencyException ex)//(OptimisticConcurrencyException) { //並發儲存錯誤 return -1; } } }
設定好屬性後,EF會幫我們自動檢測並發並拋出異常,我們用上述方法捕獲異常後,就可以執行我們重複執行的規則了,具體代碼如下:
View Code
至此,C#並發處理就講解完了,是不是很簡單呢?
項目源碼地址:http://download.csdn.net/download/wolongbb/9977216
@陳臥龍的部落格
C# 資料庫並發的解決方案(通用版、EF版)