多多点C#数据库并发的解决⽅案(通⽤版、EF版)
还是那句⽼话:⼗年河东,⼗年河西,莫欺骚年穷!~_~ 打错个字,应该是莫欺少年穷!
学历代表你的过去,能⼒代表你的现在,学习代表你的将来。
学⽆⽌境,精益求精。
⾃ASP诞⽣以来,微软提供了不少控制并发的⽅法,在了解这些控制并发的⽅法前,我们先来简单介绍下并发!
并发:同⼀时间或者同⼀时刻多个访问者同时访问某⼀更新操作时,会产⽣并发!
针对并发的处理,⼜分为悲观并发处理和乐观并发处理
所谓悲观/乐观并发处理,可以这样理解:
悲观者认为:在程序的运⾏过程中,并发很容易发⽣滴,因此,悲观者提出了他们的处理模式:在我执⾏⼀个⽅法时,不允许其他访问者介⼊这个⽅法。(悲观者经常认为某件坏事会发⽣在⾃⼰⾝上)
乐观者认为:在程序的运⾏过程中,并发是很少发⽣滴,因此,乐观者提出了他们的处理模式:在我执
⾏⼀个⽅法时,允许其他访问者介⼊这个⽅法。(乐观者经常认为某件坏事不会发⽣在⾃⼰⾝上)
那么在C#语⾔中,那些属于悲观者呢?
在C#中诸如:LOCK、Monitor、Interlocked 等锁定数据的⽅式,属于悲观并发处理范畴!数据⼀旦被锁定,其他访问者均⽆权访问。有兴趣的可以参考:
但是,悲观者处理并发的模式有⼀个通病,那就是可能会造成⾮常低下的执⾏效率。
在此:举个简单例⼦:
售票系统,⼩明去买票,要买北京到上海的D110次列车,如果采⽤悲观者处理并发的模式,那么售票员会将D110次列车的票锁定,然后再作出票操作。但是,在D110次列车车票被锁定期间,售票员去了趟厕所,或者喝了杯咖啡,其他窗⼝售票员是不能进⾏售票滴!如果采⽤这种处理⽅式的话,中国14亿⼈⼝都不⽤出⾏了,原因是买不到票 ~_~
因此:在处理数据库并发时,悲观锁还是要谨慎使⽤!具体还要看数据库并发量⼤不⼤,如果⽐较⼤,建议使⽤乐观者处理模式,如果⽐较⼩,可以适当采⽤悲观者处理模式!
OK。说了这么多,也就是做个铺垫,本节内容标题叫数据库并发的解决⽅案,我们最终还得返璞归真,从数据库并发的解决说起!
那么问题来了?
数据库并发的处理⽅式有哪些呢?
其实数据库的并发处理也是分为乐观锁和悲观锁,只不过是基于数据库层⾯⽽⾔的!关于数据库层⾯的并发处理⼤家可参考我的博客:
悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1]
乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。
最常⽤的处理多⽤户并发访问的⽅法是加锁。当⼀个⽤户锁住数据库中的某个对象时,其他⽤户就不能再访问该对象。加锁对并发访问的影响体现在锁的粒度上。⽐如,放在⼀个表上的锁限制对整个表的并发访问;放在数据页上的锁限制了对整个数据页的访问;放在⾏上的锁只限制对该⾏的并发访问。可见⾏锁粒度最⼩,并发访问最好,页锁粒度最⼤,并发访问性能就会越低。
悲观锁:假定会发⽣并发冲突,屏蔽⼀切可能违反数据完整性的操作。[1] 悲观锁假定其他⽤户企图访问或者改变你正在访问、更改的对象的概率是很⾼的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是⾏
锁,加锁的时间可能会很长,这样可能会长时间的锁定⼀个对象,限制其他⽤户的访问,也就是说悲观锁的并发访问性不好。
乐观锁:假设不会发⽣并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他⽤户企图改变你正在更改的对象的概率是很⼩的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要⽐悲观锁短,乐观锁可以⽤较⼤的锁粒度获得较好的并发访问性能。但是如果第⼆个⽤户恰好在第⼀个⽤户提交更改之前读取了该对象,那么当他完成了⾃⼰的更改进⾏提交时,数据库就会发现该对象已经变化了,这样,第⼆个⽤户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发⽤户读取对象的次数。
家电科技
本篇的主旨是讲解基于C#的数据库并发解决⽅案(通⽤版、EF版),因此我们要从C#⽅⾯⼊⼿,最好是结合⼀个⼩项⽬
项⽬已为⼤家准备好了,如下:
⾸先我们需要创建⼀个⼩型数据库:
create databa BingFaTest
go
u 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),
)
--测试数据:
inrt into Product values('苹果',1,'元/⽄',GETDATE())
梦见诈尸预示什么
inrt into Inventory(ProductId,ProductCount,InventoryTime) values(1,100,GETDATE())
View Code
创建的数据库很简单,三张表:商品表,库存表,⽇志表
有了数据库,我们就创建C#项⽬,本项⽬采⽤C# DataBaFirst 模式,结构如下:
项⽬很简单,采⽤EF DataBaFirst 模式很好构建。
项⽬构建好了,下⾯我们模拟并发的发⽣?
主要代码如下(减少库存、插⼊⽇志):
#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.Databa.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 ⼤家可以理解为版本号,版本号的作⽤是⼀旦有访问者修改数据,版本号的值就会相应发⽣改变。当然,版本号的同步更改是和数据库相关的,在SQLrver中会随着数据的修改同步更新版本号,但是在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
lect@count=ProductCount,@flag=VersionNum from Inventory where ProductId=@ProductId
统计学毕业论文update Inventory t ProductCount=@count-1where VersionNum=@flag and ProductId=@ProductId
inrt into InventoryLog values('插⼊⼀条数据,⽤于计算是否发⽣并发')
t@rowcount=@@ROWCOUNT
if@rowcount>0
t@IsSuccess=1
el
t@IsSuccess=0
commit tran
这个存储过程很简单,执⾏两个操作:减少库存和插⼊⼀条数据。有⼀个输⼊参数:productId ,⼀个输出参数,IsSuccess。如果发⽣并发,IsSuccess的值为Fal,如果执⾏成功,IsSuccess值为True。