2023年6月21日发(作者:)
C#数据库并发的解决⽅案(通⽤版、EF版)还是那句⽼话:⼗年河东,⼗年河西,莫欺骚年穷!~_~ 打错个字,应该是莫欺少年穷!学历代表你的过去,能⼒代表你的现在,学习代表你的将来。学⽆⽌境,精益求精。⾃诞⽣以来,微软提供了不少控制并发的⽅法,在了解这些控制并发的⽅法前,我们先来简单介绍下并发!并发:同⼀时间或者同⼀时刻多个访问者同时访问某⼀更新操作时,会产⽣并发!针对并发的处理,⼜分为悲观并发处理和乐观并发处理所谓悲观/乐观并发处理,可以这样理解:悲观者认为:在程序的运⾏过程中,并发很容易发⽣滴,因此,悲观者提出了他们的处理模式:在我执⾏⼀个⽅法时,不允许其他访问者介⼊这个⽅法。(悲观者经常认为某件坏事会发⽣在⾃⼰⾝上)乐观者认为:在程序的运⾏过程中,并发是很少发⽣滴,因此,乐观者提出了他们的处理模式:在我执⾏⼀个⽅法时,允许其他访问者介⼊这个⽅法。(乐观者经常认为某件坏事不会发⽣在⾃⼰⾝上)那么在C#语⾔中,那些属于悲观者呢?在C#中诸如:LOCK、Monitor、Interlocked 等锁定数据的⽅式,属于悲观并发处理范畴!数据⼀旦被锁定,其他访问者均⽆权访问。有兴趣的可以参考:但是,悲观者处理并发的模式有⼀个通病,那就是可能会造成⾮常低下的执⾏效率。在此:举个简单例⼦:售票系统,⼩明去买票,要买北京到上海的D110次列车,如果采⽤悲观者处理并发的模式,那么售票员会将D110次列车的票锁定,然后再作出票操作。但是,在D110次列车车票被锁定期间,售票员去了趟厕所,或者喝了杯咖啡,其他窗⼝售票员是不能进⾏售票滴!如果采⽤这种处理⽅式的话,中国14亿⼈⼝都不⽤出⾏了,原因是买不到票 ~_~因此:在处理数据库并发时,悲观锁还是要谨慎使⽤!具体还要看数据库并发量⼤不⼤,如果⽐较⼤,建议使⽤乐观者处理模式,如果⽐较⼩,可以适当采⽤悲观者处理模式!OK。说了这么多,也就是做个铺垫,本节内容标题叫数据库并发的解决⽅案,我们最终还得返璞归真,从数据库并发的解决说起!那么问题来了?数据库并发的处理⽅式有哪些呢?其实数据库的并发处理也是分为乐观锁和悲观锁,只不过是基于数据库层⾯⽽⾔的!关于数据库层⾯的并发处理⼤家可参考我的博客:悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1]乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 最常⽤的处理多⽤户并发访问的⽅法是加锁。当⼀个⽤户锁住数据库中的某个对象时,其他⽤户就不能再访问该对象。加锁对并发访问的影响体现在锁的粒度上。⽐如,放在⼀个表上的锁限制对整个表的并发访问;放在数据页上的锁限制了对整个数据页的访问;放在⾏上的锁只限制对该⾏的并发访问。可见⾏锁粒度最⼩,并发访问最好,页锁粒度最⼤,并发访问性能就会越低。悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1] 悲观锁假定其他⽤户企图访问或者改变你正在访问、更改的对象的概率是很⾼的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是⾏锁,加锁的时间可能会很长,这样可能会长时间的锁定⼀个对象,限制其他⽤户的访问,也就是说悲观锁的并发访问性不好。乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他⽤户企图改变你正在更改的对象的概率是很⼩的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要⽐悲观锁短,乐观锁可以⽤较⼤的锁粒度获得较好的并发访问性能。但是如果第⼆个⽤户恰好在第⼀个⽤户提交更改之前读取了该对象,那么当他完成了⾃⼰的更改进⾏提交时,数据库就会发现该对象已经变化了,这样,第⼆个⽤户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发⽤户读取对象的次数。本篇的主旨是讲解基于C#的数据库并发解决⽅案(通⽤版、EF版),因此我们要从C#⽅⾯⼊⼿,最好是结合⼀个⼩项⽬项⽬已为⼤家准备好了,如下:⾸先我们需要创建⼀个⼩型数据库:
create database BingFaTestgouse BingFaTestgo
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())View Code创建的数据库很简单,三张表:商品表,库存表,⽇志表有了数据库,我们就创建C#项⽬,本项⽬采⽤C# DataBaseFirst 模式,结构如下:项⽬很简单,采⽤EF DataBaseFirst 模式很好构建。 项⽬构建好了,下⾯我们模拟并发的发⽣?主要代码如下(减少库存、插⼊⽇志):#region 未做并发处理 ///
@IsSuccess bit=0 output)asdeclare @count as intdeclare @flag as TimeStampdeclare @rowcount As int
begin transelect @count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId
update Inventory set ProductCount=@count-1 where VersionNum=@flag and ProductId=@ProductIdinsert into InventoryLog values('插⼊⼀条数据,⽤于计算是否发⽣并发')set @rowcount=@@ROWCOUNTif @rowcount>0set @IsSuccess=1elseset @IsSuccess=0commit tran 这个存储过程很简单,执⾏两个操作:减少库存和插⼊⼀条数据。有⼀个输⼊参数:productId ,⼀个输出参数,IsSuccess。如果发⽣并发,IsSuccess的值为False,如果执⾏成功,IsSuccess值为True。在这⾥,向⼤家说明⼀点:程序采⽤悲观锁,是串⾏的,采⽤乐观锁,是并⾏的。也就是说:采⽤悲观锁,⼀次仅执⾏⼀个访问者的请求,待前⼀个访问者访问完成并释放锁时,下⼀个访问者会依次进⼊锁定的程序并执⾏,直到所有访问者执⾏结束。因此,悲观锁严格按照次序执⾏的模式能保证所有访问者执⾏成功。采⽤乐观锁时,访问者是并⾏执⾏的,⼤家同时访问⼀个⽅法,只不过同⼀时刻只会有⼀个访问者操作成功,其他访问者执⾏失败。那么,针对这些执⾏失败的访问者怎么处理呢?直接返回失败信息是不合理的,⽤户体验不好,因此,需要定制⼀个规则,让执⾏失败的访问者重新执⾏之前的请求即可。 时间有限,就不多写了...因为并发的控制是在数据库端存储过程,所以,C#代码也很简单。如下:
#region 通⽤并发处理模式 存储过程实现 ///
Edmx⽂件⽤记事本打开如下: Provider="ent" ProviderManifestToken="2008" xmlns:store="/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="/ado/2009/11/edm/ssdl"> store:Type="Tables" Schema="dbo" /> Schema="dbo" /> Role="Product" EntitySet="Product" /> Name="Inventory"> StoreGeneratedPattern="Identity" /> Name="VersionNum" Type="timestamp" Nullable="false" StoreGeneratedPattern="Computed" /> Nullable="false" StoreGeneratedPattern="Identity" /> Name="Product"> StoreGeneratedPattern="Identity" /> Type="money" /> Multiplicity="0..1" /> Role="Product"> Name="ProductId" /> xmlns:annotation="/ado/2009/02/edm/annotation" xmlns:p1="/ado/2009/02/edm/annotation" xmlns="/ado/2009/11/edm"> Name="Inventory" EntityType="ory" /> Association="__Inventory__Produ__145C0A3F"> EntitySet="Inventory" /> Name="InventoryId" /> Nullable="false" MaxLength="8" FixedLength="true" p1:StoreGeneratedPattern="Computed" ConcurrencyMode="None" /> Name="InventoryTime" Type="DateTime" Precision="3" /> Relationship="__Inventory__Produ__145C0A3F" FromRole="Inventory" ToRole="Product" /> Name="InventoryLog"> p1:StoreGeneratedPattern="Identity" /> Type="Int32" Nullable="false" p1:StoreGeneratedPattern="Identity" /> FixedLength="false" /> Type="String" MaxLength="10" Unicode="true" FixedLength="false" /> Multiplicity="0..1" /> Role="Product"> Name="ProductId" />
Name="Inventory">
ColumnName="Title" />
Name="ProductId" ColumnName="ProductId" /> Name="ProductPrice" ColumnName="ProductPrice" />
Name="MetadataArtifactProcessing" Value="EmbedInOutputAssembly" />
Value="False" /> Value="⽆" />
View Code其实,在EF DataBaseFirst中,我们只需设置下类型为 TimeStamp 版本号的属性即可,如下:设置好了版本号属性后,你就可以进⾏并发测试了,当系统发⽣并发时,程序会抛出异常,⽽我们要做的就是要捕获这个异常,⽽后就是按照⾃⼰的规则,重复执⾏请求的⽅法,直⾄返回成功为⽌。那么如何捕获并发异常呢?在C#代码中需要使⽤异常类:DbUpdateConcurrencyException 来捕获,EF中具体⽤法如下:public class SaveChangesForBF : BingFaTestEntities { public override int SaveChanges() { try { return anges(); } catch (DbUpdateConcurrencyException ex)//(OptimisticConcurrencyException) { //并发保存错误 return -1; } } }设置好属性后,EF会帮我们⾃动检测并发并抛出异常,我们⽤上述⽅法捕获异常后,就可以执⾏我们重复执⾏的规则了,具体代码如下: #region EF专属并发处理模式 ///
}; (LogModel); anges(); //1.5 模拟耗时 (500); //消耗半秒钟 (); } } return C; } #endregionView Code ⾄此,C#并发处理就讲解完了,是不是很简单呢?@陈卧龙的博客
2023年6月21日发(作者:)
C#数据库并发的解决⽅案(通⽤版、EF版)还是那句⽼话:⼗年河东,⼗年河西,莫欺骚年穷!~_~ 打错个字,应该是莫欺少年穷!学历代表你的过去,能⼒代表你的现在,学习代表你的将来。学⽆⽌境,精益求精。⾃诞⽣以来,微软提供了不少控制并发的⽅法,在了解这些控制并发的⽅法前,我们先来简单介绍下并发!并发:同⼀时间或者同⼀时刻多个访问者同时访问某⼀更新操作时,会产⽣并发!针对并发的处理,⼜分为悲观并发处理和乐观并发处理所谓悲观/乐观并发处理,可以这样理解:悲观者认为:在程序的运⾏过程中,并发很容易发⽣滴,因此,悲观者提出了他们的处理模式:在我执⾏⼀个⽅法时,不允许其他访问者介⼊这个⽅法。(悲观者经常认为某件坏事会发⽣在⾃⼰⾝上)乐观者认为:在程序的运⾏过程中,并发是很少发⽣滴,因此,乐观者提出了他们的处理模式:在我执⾏⼀个⽅法时,允许其他访问者介⼊这个⽅法。(乐观者经常认为某件坏事不会发⽣在⾃⼰⾝上)那么在C#语⾔中,那些属于悲观者呢?在C#中诸如:LOCK、Monitor、Interlocked 等锁定数据的⽅式,属于悲观并发处理范畴!数据⼀旦被锁定,其他访问者均⽆权访问。有兴趣的可以参考:但是,悲观者处理并发的模式有⼀个通病,那就是可能会造成⾮常低下的执⾏效率。在此:举个简单例⼦:售票系统,⼩明去买票,要买北京到上海的D110次列车,如果采⽤悲观者处理并发的模式,那么售票员会将D110次列车的票锁定,然后再作出票操作。但是,在D110次列车车票被锁定期间,售票员去了趟厕所,或者喝了杯咖啡,其他窗⼝售票员是不能进⾏售票滴!如果采⽤这种处理⽅式的话,中国14亿⼈⼝都不⽤出⾏了,原因是买不到票 ~_~因此:在处理数据库并发时,悲观锁还是要谨慎使⽤!具体还要看数据库并发量⼤不⼤,如果⽐较⼤,建议使⽤乐观者处理模式,如果⽐较⼩,可以适当采⽤悲观者处理模式!OK。说了这么多,也就是做个铺垫,本节内容标题叫数据库并发的解决⽅案,我们最终还得返璞归真,从数据库并发的解决说起!那么问题来了?数据库并发的处理⽅式有哪些呢?其实数据库的并发处理也是分为乐观锁和悲观锁,只不过是基于数据库层⾯⽽⾔的!关于数据库层⾯的并发处理⼤家可参考我的博客:悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1]乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 最常⽤的处理多⽤户并发访问的⽅法是加锁。当⼀个⽤户锁住数据库中的某个对象时,其他⽤户就不能再访问该对象。加锁对并发访问的影响体现在锁的粒度上。⽐如,放在⼀个表上的锁限制对整个表的并发访问;放在数据页上的锁限制了对整个数据页的访问;放在⾏上的锁只限制对该⾏的并发访问。可见⾏锁粒度最⼩,并发访问最好,页锁粒度最⼤,并发访问性能就会越低。悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1] 悲观锁假定其他⽤户企图访问或者改变你正在访问、更改的对象的概率是很⾼的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是⾏锁,加锁的时间可能会很长,这样可能会长时间的锁定⼀个对象,限制其他⽤户的访问,也就是说悲观锁的并发访问性不好。乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他⽤户企图改变你正在更改的对象的概率是很⼩的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要⽐悲观锁短,乐观锁可以⽤较⼤的锁粒度获得较好的并发访问性能。但是如果第⼆个⽤户恰好在第⼀个⽤户提交更改之前读取了该对象,那么当他完成了⾃⼰的更改进⾏提交时,数据库就会发现该对象已经变化了,这样,第⼆个⽤户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发⽤户读取对象的次数。本篇的主旨是讲解基于C#的数据库并发解决⽅案(通⽤版、EF版),因此我们要从C#⽅⾯⼊⼿,最好是结合⼀个⼩项⽬项⽬已为⼤家准备好了,如下:⾸先我们需要创建⼀个⼩型数据库:
create database BingFaTestgouse BingFaTestgo
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())View Code创建的数据库很简单,三张表:商品表,库存表,⽇志表有了数据库,我们就创建C#项⽬,本项⽬采⽤C# DataBaseFirst 模式,结构如下:项⽬很简单,采⽤EF DataBaseFirst 模式很好构建。 项⽬构建好了,下⾯我们模拟并发的发⽣?主要代码如下(减少库存、插⼊⽇志):#region 未做并发处理 ///
@IsSuccess bit=0 output)asdeclare @count as intdeclare @flag as TimeStampdeclare @rowcount As int
begin transelect @count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId
update Inventory set ProductCount=@count-1 where VersionNum=@flag and ProductId=@ProductIdinsert into InventoryLog values('插⼊⼀条数据,⽤于计算是否发⽣并发')set @rowcount=@@ROWCOUNTif @rowcount>0set @IsSuccess=1elseset @IsSuccess=0commit tran 这个存储过程很简单,执⾏两个操作:减少库存和插⼊⼀条数据。有⼀个输⼊参数:productId ,⼀个输出参数,IsSuccess。如果发⽣并发,IsSuccess的值为False,如果执⾏成功,IsSuccess值为True。在这⾥,向⼤家说明⼀点:程序采⽤悲观锁,是串⾏的,采⽤乐观锁,是并⾏的。也就是说:采⽤悲观锁,⼀次仅执⾏⼀个访问者的请求,待前⼀个访问者访问完成并释放锁时,下⼀个访问者会依次进⼊锁定的程序并执⾏,直到所有访问者执⾏结束。因此,悲观锁严格按照次序执⾏的模式能保证所有访问者执⾏成功。采⽤乐观锁时,访问者是并⾏执⾏的,⼤家同时访问⼀个⽅法,只不过同⼀时刻只会有⼀个访问者操作成功,其他访问者执⾏失败。那么,针对这些执⾏失败的访问者怎么处理呢?直接返回失败信息是不合理的,⽤户体验不好,因此,需要定制⼀个规则,让执⾏失败的访问者重新执⾏之前的请求即可。 时间有限,就不多写了...因为并发的控制是在数据库端存储过程,所以,C#代码也很简单。如下:
#region 通⽤并发处理模式 存储过程实现 ///
Edmx⽂件⽤记事本打开如下: Provider="ent" ProviderManifestToken="2008" xmlns:store="/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="/ado/2009/11/edm/ssdl"> store:Type="Tables" Schema="dbo" /> Schema="dbo" /> Role="Product" EntitySet="Product" /> Name="Inventory"> StoreGeneratedPattern="Identity" /> Name="VersionNum" Type="timestamp" Nullable="false" StoreGeneratedPattern="Computed" /> Nullable="false" StoreGeneratedPattern="Identity" /> Name="Product"> StoreGeneratedPattern="Identity" /> Type="money" /> Multiplicity="0..1" /> Role="Product"> Name="ProductId" /> xmlns:annotation="/ado/2009/02/edm/annotation" xmlns:p1="/ado/2009/02/edm/annotation" xmlns="/ado/2009/11/edm"> Name="Inventory" EntityType="ory" /> Association="__Inventory__Produ__145C0A3F"> EntitySet="Inventory" /> Name="InventoryId" /> Nullable="false" MaxLength="8" FixedLength="true" p1:StoreGeneratedPattern="Computed" ConcurrencyMode="None" /> Name="InventoryTime" Type="DateTime" Precision="3" /> Relationship="__Inventory__Produ__145C0A3F" FromRole="Inventory" ToRole="Product" /> Name="InventoryLog"> p1:StoreGeneratedPattern="Identity" /> Type="Int32" Nullable="false" p1:StoreGeneratedPattern="Identity" /> FixedLength="false" /> Type="String" MaxLength="10" Unicode="true" FixedLength="false" /> Multiplicity="0..1" /> Role="Product"> Name="ProductId" />
Name="Inventory">
ColumnName="Title" />
Name="ProductId" ColumnName="ProductId" /> Name="ProductPrice" ColumnName="ProductPrice" />
Name="MetadataArtifactProcessing" Value="EmbedInOutputAssembly" />
Value="False" /> Value="⽆" />
View Code其实,在EF DataBaseFirst中,我们只需设置下类型为 TimeStamp 版本号的属性即可,如下:设置好了版本号属性后,你就可以进⾏并发测试了,当系统发⽣并发时,程序会抛出异常,⽽我们要做的就是要捕获这个异常,⽽后就是按照⾃⼰的规则,重复执⾏请求的⽅法,直⾄返回成功为⽌。那么如何捕获并发异常呢?在C#代码中需要使⽤异常类:DbUpdateConcurrencyException 来捕获,EF中具体⽤法如下:public class SaveChangesForBF : BingFaTestEntities { public override int SaveChanges() { try { return anges(); } catch (DbUpdateConcurrencyException ex)//(OptimisticConcurrencyException) { //并发保存错误 return -1; } } }设置好属性后,EF会帮我们⾃动检测并发并抛出异常,我们⽤上述⽅法捕获异常后,就可以执⾏我们重复执⾏的规则了,具体代码如下: #region EF专属并发处理模式 ///
}; (LogModel); anges(); //1.5 模拟耗时 (500); //消耗半秒钟 (); } } return C; } #endregionView Code ⾄此,C#并发处理就讲解完了,是不是很简单呢?@陈卧龙的博客
发布评论