天天看點

C# 資料庫并發的解決方案(通用版、EF版)

還是那句老話:十年河東,十年河西,莫欺騷年窮!~_~ 打錯個字,應該是莫欺少年窮!

學曆代表你的過去,能力代表你的現在,學習代表你的将來。

學無止境,精益求精。

自ASP.NET誕生以來,微軟提供了不少控制并發的方法,在了解這些控制并發的方法前,我們先來簡單介紹下并發!

并發:同一時間或者同一時刻多個通路者同時通路某一更新操作時,會産生并發!

針對并發的處理,又分為悲觀并發處理和樂觀并發處理

所謂悲觀/樂觀并發處理,可以這樣了解:

悲觀者認為:在程式的運作過程中,并發很容易發生滴,是以,悲觀者提出了他們的處理模式:在我執行一個方法時,不允許其他通路者介入這個方法。(悲觀者經常認為某件壞事會發生在自己身上)

樂觀者認為:在程式的運作過程中,并發是很少發生滴,是以,樂觀者提出了他們的處理模式:在我執行一個方法時,允許其他通路者介入這個方法。(樂觀者經常認為某件壞事不會發生在自己身上)

那麼在C#語言中,那些屬于悲觀者呢?

但是,悲觀者處理并發的模式有一個通病,那就是可能會造成非常低下的執行效率。

在此:舉個簡單例子:

售票系統,小明去買票,要買北京到上海的D110次列車,如果采用悲觀者處理并發的模式,那麼售票員會将D110次列車的票鎖定,然後再作出票操作。但是,在D110次列車車票被鎖定期間,售票員去了趟廁所,或者喝了杯咖啡,其他視窗售票員是不能進行售票滴!如果采用這種處理方式的話,中國14億人口都不用出行了,原因是買不到票 ~_~

是以:在處理資料庫并發時,悲觀鎖還是要謹慎使用!具體還要看資料庫并發量大不大,如果比較大,建議使用樂觀者處理模式,如果比較小,可以适當采用悲觀者處理模式!

OK。說了這麼多,也就是做個鋪墊,本節内容标題叫資料庫并發的解決方案,我們最終還得返璞歸真,從資料庫并發的解決說起!

那麼問題來了?

資料庫并發的處理方式有哪些呢?

悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反資料完整性的操作。[1]

樂觀鎖:假設不會發生并發沖突,隻在送出操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。

 最常用的處理多使用者并發通路的方法是加鎖。當一個使用者鎖住資料庫中的某個對象時,其他使用者就不能再通路該對象。加鎖對并發通路的影響展現在鎖的粒度上。比如,放在一個表上的鎖限制對整個表的并發通路;放在資料頁上的鎖限制了對整個資料頁的通路;放在行上的鎖隻限制對該行的并發通路。可見行鎖粒度最小,并發通路最好,頁鎖粒度最大,并發通路性能就會越低。

悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反資料完整性的操作。[1] 悲觀鎖假定其他使用者企圖通路或者改變你正在通路、更改的對象的機率是很高的,是以在悲觀鎖的環境中,在你開始改變此對象之前就将該對象鎖住,并且直到你送出了所作的更改之後才釋放鎖。悲觀的缺陷是不論是頁鎖還是行鎖,加鎖的時間可能會很長,這樣可能會長時間的鎖定一個對象,限制其他使用者的通路,也就是說悲觀鎖的并發通路性不好。

樂觀鎖:假設不會發生并發沖突,隻在送出操作時檢查是否違反資料完整性。[1] 樂觀鎖不能解決髒讀的問題。 樂觀鎖則認為其他使用者企圖改變你正在更改的對象的機率是很小的,是以樂觀鎖直到你準備送出所作的更改時才将對象鎖住,當你讀取以及改變該對象時并不加鎖。可見樂觀鎖加鎖的時間要比悲觀鎖短,樂觀鎖可以用較大的鎖粒度獲得較好的并發通路性能。但是如果第二個使用者恰好在第一個使用者送出更改之前讀取了該對象,那麼當他完成了自己的更改進行送出時,資料庫就會發現該對象已經變化了,這樣,第二個使用者不得不重新讀取該對象并作出更改。這說明在樂觀鎖環境中,會增加并發使用者讀取對象的次數。

本篇的主旨是講解基于C#的資料庫并發解決方案(通用版、EF版),是以我們要從C#方面入手,最好是結合一個小項目

項目已為大家準備好了,如下:

首先我們需要建立一個小型資料庫:

C# 資料庫并發的解決方案(通用版、EF版)
C# 資料庫并發的解決方案(通用版、EF版)

View Code

建立的資料庫很簡單,三張表:商品表,庫存表,日志表

有了資料庫,我們就建立C#項目,本項目采用C# DataBaseFirst 模式,結構如下:

C# 資料庫并發的解決方案(通用版、EF版)

項目很簡單,采用EF DataBaseFirst 模式很好建構。

 項目建構好了,下面我們模拟并發的發生?

主要代碼如下(減少庫存、插入日志):

此時我們 int productId=1 處加上斷點,并運作程式(打開四個浏覽器同時執行),如下:

C# 資料庫并發的解決方案(通用版、EF版)

由上圖可知,四個通路者同時通路這個未采用并發控制的方法,得到的結果如下:

C# 資料庫并發的解決方案(通用版、EF版)

結果顯示:日志生成四條資料,而庫存量缺隻減少1個。這個結果顯然是不正确的,原因是因為發生了并發,其本質原因是髒讀,誤讀,不可重讀造成的。

那麼,問題既然發生了,我們就想辦法法解決,辦法有兩種,分别為:悲觀鎖方法、樂觀鎖方法。

悲觀者方法:

悲觀者方法(加了uodlock鎖,鎖定了更新操作,也就是說,一旦被鎖定,其他通路者不允許通路此操作)類似這種方法,可以通過存儲過程實作,在此不作解釋了

C# 資料庫并發的解決方案(通用版、EF版)

樂觀者方法(通用版/存儲過程實作):

在上述資料庫腳本中,有字段叫做:VersionNum,類型為:TimeStamp。

字段 VersionNum 大家可以了解為版本号,版本号的作用是一旦有通路者修改資料,版本号的值就會相應發生改變。當然,版本号的同步更改是和資料庫相關的,在SQLserver中會随着資料的修改同步更新版本号,但是在MySQL裡就不會随着資料的修改而更改。是以,如果你采用的是MYSQL資料庫,就需要寫一個觸發器,如下:

C# 資料庫并發的解決方案(通用版、EF版)
C# 資料庫并發的解決方案(通用版、EF版)

OK,了解了類型為Timestamp的字段,下面我們結合上述的小型資料庫建立一個處理并發的存儲過程,如下

 這個存儲過程很簡單,執行兩個操作:減少庫存和插入一條資料。有一個輸入參數:productId ,一個輸出參數,IsSuccess。如果發生并發,IsSuccess的值為False,如果執行成功,IsSuccess值為True。

在這裡,向大家說明一點:程式采用悲觀鎖,是串行的,采用樂觀鎖,是并行的。

也就是說:采用悲觀鎖,一次僅執行一個通路者的請求,待前一個通路者通路完成并釋放鎖時,下一個通路者會依次進入鎖定的程式并執行,直到所有通路者執行結束。是以,悲觀鎖嚴格按照次序執行的模式能保證所有通路者執行成功。

采用樂觀鎖時,通路者是并行執行的,大家同時通路一個方法,隻不過同一時刻隻會有一個通路者操作成功,其他通路者執行失敗。那麼,針對這些執行失敗的通路者怎麼處理呢?直接傳回失敗資訊是不合理的,使用者體驗不好,是以,需要定制一個規則,讓執行失敗的通路者重新執行之前的請求即可。

 時間有限,就不多寫了...因為并發的控制是在資料庫端存儲過程,是以,C#代碼也很簡單。如下:

C# 資料庫并發的解決方案(通用版、EF版)
C# 資料庫并發的解決方案(通用版、EF版)

在此,需要說明如下:

當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找看到:

C# 資料庫并發的解決方案(通用版、EF版)

Edmx檔案用記事本打開如下:

C# 資料庫并發的解決方案(通用版、EF版)
C# 資料庫并發的解決方案(通用版、EF版)

其實,在EF DataBaseFirst中,我們隻需設定下類型為 TimeStamp 版本号的屬性即可,如下:

C# 資料庫并發的解決方案(通用版、EF版)

設定好了版本号屬性後,你就可以進行并發測試了,當系統發生并發時,程式會抛出異常,而我們要做的就是要捕獲這個異常,而後就是按照自己的規則,重複執行請求的方法,直至傳回成功為止。

那麼如何捕獲并發異常呢?

在C#代碼中需要使用異常類:DbUpdateConcurrencyException 來捕獲,EF中具體用法如下:

設定好屬性後,EF會幫我們自動檢測并發并抛出異常,我們用上述方法捕獲異常後,就可以執行我們重複執行的規則了,具體代碼如下:

C# 資料庫并發的解決方案(通用版、EF版)
C# 資料庫并發的解決方案(通用版、EF版)

 至此,C#并發處理就講解完了,是不是很簡單呢?

@陳卧龍的部落格