C# 數據庫併發的解決方案(通用版、EF版)

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

學歷表明你的過去,能力表明你的如今,學習表明你的未來。數據庫

學無止境,精益求精。瀏覽器

自ASP.NET誕生以來,微軟提供了很多控制併發的方法,在瞭解這些控制併發的方法前,咱們先來簡單介紹下併發!併發

併發:同一時間或者同一時刻多個訪問者同時訪問某一更新操做時,會產生併發!框架

針對併發的處理,又分爲悲觀併發處理和樂觀併發處理ide

所謂悲觀/樂觀併發處理,能夠這樣理解:性能

悲觀者認爲:在程序的運行過程當中,併發很容易發生滴,所以,悲觀者提出了他們的處理模式:在我執行一個方法時,不容許其餘訪問者介入這個方法。(悲觀者常常認爲某件壞事會發生在本身身上)學習

樂觀者認爲:在程序的運行過程當中,併發是不多發生滴,所以,樂觀者提出了他們的處理模式:在我執行一個方法時,容許其餘訪問者介入這個方法。(樂觀者常常認爲某件壞事不會發生在本身身上)測試

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

在C#中諸如:LOCK、Monitor、Interlocked 等鎖定數據的方式,屬於悲觀併發處理範疇!數據一旦被鎖定,其餘訪問者均無權訪問。有興趣的能夠參考:鎖、C#中Monitor和Lock以及區別

可是,悲觀者處理併發的模式有一個通病,那就是可能會形成很是低下的執行效率。

在此:舉個簡單例子:

售票系統,小明去買票,要買北京到上海的D110次列車,若是採用悲觀者處理併發的模式,那麼售票員會將D110次列車的票鎖定,而後再做出票操做。可是,在D110次列車車票被鎖按期間,售票員去了趟廁所,或者喝了杯咖啡,其餘窗口售票員是不能進行售票滴!若是採用這種處理方式的話,中國14億人口都不用出行了,緣由是買不到票 ~_~

所以:在處理數據庫併發時,悲觀鎖仍是要謹慎使用!具體還要看數據庫併發量大不大,若是比較大,建議使用樂觀者處理模式,若是比較小,能夠適當採用悲觀者處理模式!

OK。說了這麼多,也就是作個鋪墊,本節內容標題叫數據庫併發的解決方案,咱們最終還得返璞歸真,從數據庫併發的解決提及!

那麼問題來了?

數據庫併發的處理方式有哪些呢?

其實數據庫的併發處理也是分爲樂觀鎖和悲觀鎖,只不過是基於數據庫層面而言的!關於數據庫層面的併發處理你們可參考個人博客:樂觀鎖悲觀鎖應用

悲觀鎖:假定會發生併發衝突,屏蔽一切可能違反數據完整性的操做。[1]

樂觀鎖:假設不會發生併發衝突,只在提交操做時檢查是否違反數據完整性。[1] 樂觀鎖不能解決髒讀的問題。

 最經常使用的處理多用戶併發訪問的方法是加鎖。當一個用戶鎖住數據庫中的某個對象時,其餘用戶就不能再訪問該對象。加鎖對併發訪問的影響體如今鎖的粒度上。好比,放在一個表上的鎖限制對整個表的併發訪問;放在數據頁上的鎖限制了對整個數據頁的訪問;放在行上的鎖只限制對該行的併發訪問。可見行鎖粒度最小,併發訪問最好,頁鎖粒度最大,併發訪問性能就會越低。

悲觀鎖:假定會發生併發衝突,屏蔽一切可能違反數據完整性的操做。[1] 悲觀鎖假定其餘用戶企圖訪問或者改變你正在訪問、更改的對象的機率是很高的,所以在悲觀鎖的環境中,在你開始改變此對象以前就將該對象鎖住,而且直到你提交了所做的更改以後才釋放鎖。悲觀的缺陷是不管是頁鎖仍是行鎖,加鎖的時間可能會很長,這樣可能會長時間的鎖定一個對象,限制其餘用戶的訪問,也就是說悲觀鎖的併發訪問性很差。

樂觀鎖:假設不會發生併發衝突,只在提交操做時檢查是否違反數據完整性。[1] 樂觀鎖不能解決髒讀的問題。 樂觀鎖則認爲其餘用戶企圖改變你正在更改的對象的機率是很小的,所以樂觀鎖直到你準備提交所做的更改時纔將對象鎖住,當你讀取以及改變該對象時並不加鎖。可見樂觀鎖加鎖的時間要比悲觀鎖短,樂觀鎖能夠用較大的鎖粒度得到較好的併發訪問性能。可是若是第二個用戶剛好在第一個用戶提交更改以前讀取了該對象,那麼當他完成了本身的更改進行提交時,數據庫就會發現該對象已經變化了,這樣,第二個用戶不得不從新讀取該對象並做出更改。這說明在樂觀鎖環境中,會增長併發用戶讀取對象的次數。

本篇的主旨是講解基於C#的數據庫併發解決方案(通用版、EF版),所以咱們要從C#方面入手,最好是結合一個小項目

項目已爲你們準備好了,以下:

首先咱們須要建立一個小型數據庫:

複製代碼

create database  BingFaTest
go
use BingFaTest
go 
create table Product--商品表
(
ProductId int identity(1,1) primary key,--商品ID 主鍵
ProductName nvarchar(50),--商品名稱
ProductPrice money,--單價
ProductUnit nvarchar(10) default('元/斤'),
AddTime datetime default(getdate())--添加時間

)


create table Inventory--庫存表
(
InventoryId int identity(1,1) primary key,
ProductId int FOREIGN KEY REFERENCES Product(ProductId), --外鍵
ProductCount int,--庫存數量
VersionNum TimeStamp not null,
InventoryTime datetime default(getdate()),--時間
)

create table InventoryLog
(
Id int identity(1,1) primary key,
Title nvarchar(50),
)


--測試數據:
insert into Product values('蘋果',1,'元/斤',GETDATE())


insert into Inventory(ProductId,ProductCount,InventoryTime) values(1,100,GETDATE())

複製代碼

建立的數據庫很簡單,三張表:商品表,庫存表,日誌表

有了數據庫,咱們就建立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
)
as
declare @count as int
declare @flag as TimeStamp
declare @rowcount As int 
begin tran
select @count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId
 
update Inventory set ProductCount=@count-1 where VersionNum=@flag and ProductId=@ProductId
insert into InventoryLog values('插入一條數據,用於計算是否發生併發')
set @rowcount=@@ROWCOUNT
if @rowcount>0
set @IsSuccess=1
else
set @IsSuccess=0
commit tran

複製代碼

 這個存儲過程很簡單,執行兩個操做:減小庫存和插入一條數據。有一個輸入參數:productId ,一個輸出參數,IsSuccess。若是發生併發,IsSuccess的值爲False,若是執行成功,IsSuccess值爲True。

在這裏,向你們說明一點:程序採用悲觀鎖,是串行的,採用樂觀鎖,是並行的。

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

採用樂觀鎖時,訪問者是並行執行的,你們同時訪問一個方法,只不過同一時刻只會有一個訪問者操做成功,其餘訪問者執行失敗。那麼,針對這些執行失敗的訪問者怎麼處理呢?直接返回失敗信息是不合理的,用戶體驗很差,所以,須要定製一個規則,讓執行失敗的訪問者從新執行以前的請求便可。

 時間有限,就很少寫了...由於併發的控制是在數據庫端存儲過程,因此,C#代碼也很簡單。以下:

複製代碼

#region 通用併發處理模式 存儲過程實現
        /// <summary>
        /// 存儲過程實現
        /// </summary>
        public void SubMitOrder_2()
        {
            int productId = 1;
            bool bol = LockForPorcduce(productId);
            //1.5  模擬耗時
            Thread.Sleep(500); //消耗半秒鐘
            int retry = 10;
            while (!bol && retry > 0)
            {
                retry--;
                LockForPorcduce(productId);
            }
        }


        private bool LockForPorcduce(int ProductId)
        {
            using (BingFaTestEntities context = new BingFaTestEntities())
            {
                SqlParameter[] parameters = {
                    new SqlParameter("@ProductId", SqlDbType.Int),
                    new SqlParameter("@IsSuccess", SqlDbType.Bit)
                    };
                parameters[0].Value = ProductId;
                parameters[1].Direction = ParameterDirection.Output;
                var data = context.Database.ExecuteSqlCommand("exec LockProc @ProductId,@IsSuccess output", parameters);
                string n2 = parameters[1].Value.ToString();
                if (n2 == "True")
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
        }
        #endregion

複製代碼

在此,須要說明以下:

當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會幫咱們自動檢測併發並拋出異常,咱們用上述方法捕獲異常後,就能夠執行咱們重複執行的規則了,具體代碼以下:

複製代碼

#region EF專屬併發處理模式
        /// <summary>
        /// 存儲過程實現
        /// </summary>
        public void SubMitOrder()
        {
            int C = LockForEF();
            //1.5  模擬耗時
            Thread.Sleep(500); //消耗半秒鐘
            int retry = 10;
            while (C<0 && retry > 0)
            {
                retry--;
                C= LockForEF();
            }
        }
        /// <summary>
        /// 模仿一個減小庫存操做  EF專屬併發處理模式
        /// </summary>
        public int LockForEF()
        {
            int productId = 1;
            int C = 0;
            using (SaveChangesForBF context = new SaveChangesForBF())
            {
                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;
                    C = context.SaveChanges();
                    //插入日誌
                    InventoryLog LogModel = new InventoryLog()
                    {
                        Title = "插入一條數據,用於計算是否發生併發",
                        
                    };
                    InventoryLogDbSet.Add(LogModel);
                    context.SaveChanges();
                    //1.5  模擬耗時
                    Thread.Sleep(500); //消耗半秒鐘
                    Transaction.Commit();
                }

            }
            return C;
        }
        #endregion

複製代碼

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

項目源碼地址:http://download.csdn.net/download/wolongbb/9977216

相關文章
相關標籤/搜索