千家信息网

mysql数据库端处理并发的方法

发表于:2024-11-11 作者:千家信息网编辑
千家信息网最后更新 2024年11月11日,这篇文章主要介绍"mysql数据库端处理并发的方法",在日常操作中,相信很多人在mysql数据库端处理并发的方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"mysql
千家信息网最后更新 2024年11月11日mysql数据库端处理并发的方法

这篇文章主要介绍"mysql数据库端处理并发的方法",在日常操作中,相信很多人在mysql数据库端处理并发的方法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"mysql数据库端处理并发的方法"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

自ASP.NET诞生以来,微软提供了不少控制并发的方法,在了解这些控制并发的方法前,我们先来简单介绍下并发!

并发:同一时间或者同一时刻多个访问者同时访问某一更新操作时,会产生并发!

针对并发的处理,又分为悲观并发处理和乐观并发处理

所谓悲观/乐观并发处理,可以这样理解:

悲观者认为:在程序的运行过程中,并发很容易发生滴,因此,悲观者提出了他们的处理模式:在我执行一个方法时,不允许其他访问者介入这个方法。(悲观者经常认为某件坏事会发生在自己身上)

乐观者认为:在程序的运行过程中,并发是很少发生滴,因此,乐观者提出了他们的处理模式:在我执行一个方法时,允许其他访问者介入这个方法。(乐观者经常认为某件坏事不会发生在自己身上)

那么在C#语言中,那些属于悲观者呢?

在C#中诸如:LOCK、Monitor、Interlocked 等锁定数据的方式,属于悲观并发处理范畴!数据一旦被锁定,其他访问者均无权访问。有兴趣的可以参考:锁、C#中Monitor和Lock以及区别

但是,悲观者处理并发的模式有一个通病,那就是可能会造成非常低下的执行效率。

在此:举个简单例子:

售票系统,小明去买票,要买北京到上海的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())

创建的数据库很简单,三张表:商品表,库存表,日志表

有了数据库,我们就创建C#项目,本项目采用C# DataBaseFirst 模式,结构如下:

项目很简单,采用EF DataBaseFirst 模式很好构建。

项目构建好了,下面我们模拟并发的发生?

主要代码如下(减少库存、插入日志):

#region 未做并发处理        ///         /// 模仿一个减少库存操作  不加并发控制        ///         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锁,锁定了更新操作,也就是说,一旦被锁定,其他访问者不允许访问此操作)类似这种方法,可以通过存储过程实现,在此不作解释了

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 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 通用并发处理模式 存储过程实现        ///         /// 存储过程实现        ///         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找看到:

其实,在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专属并发处理模式        ///         /// 存储过程实现        ///         public void SubMitOrder()        {            int C = LockForEF();            //1.5  模拟耗时            Thread.Sleep(500); //消耗半秒钟            int retry = 10;            while (C<0 && retry > 0)            {                retry--;                C= LockForEF();            }        }        ///         /// 模仿一个减少库存操作  EF专属并发处理模式        ///         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

====================自己的,程序处理并发==================================

使用async/await,创建异步函数,创建一个子线程,使用await依次执行完成

到此,关于"mysql数据库端处理并发的方法"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

0