第二十节: 深入理解并发机制以及解决方案(锁机制、EF自有机制、队列模式等)

一. 理解并发机制

1. 什么是并发,并发与多线程有什么关系?

①. 先从广义上来说,或者从实际场景上来说.

  高并发通常是海量用户同时访问(比如:12306买票、淘宝的双十一抢购),如果把一个用户看做一个线程的话那么并发可以理解成多线程同时访问,高并发即海量线程同时访问。

      (ps:我们在这里模拟高并发可以for循环多个线程即可)

②.从代码或数据的层次上来说.

  多个线程同时在一条相同的数据上执行多个数据库操作。

2. 从代码层次上来说,给并发分类。

①.积极并发(乐观并发、乐观锁):无论何时从数据库请求数据,数据都会被读取并保存到应用内存中。数据库级别没有放置任何显式锁。数据操作会按照数据层接收到的先后顺序来执行。

 积极并发本质就是允许冲突发生,然后在代码本身采取一种合理的方式去解决这个并发冲突,常见的方式有:

a.忽略冲突强制更新:数据库会保存最后一次更新操作(以更新为例),会损失很多用户的更新操作。

b.部分更新:允许所有的更改,但是不允许更新完整的行,只有特定用户拥有的列更新了。这就意味着,如果两个用户更新相同的记录但却不同的列,那么这两个更新都会成功,而且来自这两个用户的更改都是可见的。(EF默认实现不了这种情况)

c.询问用户:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,这时应用程序就会警告该用户该数据已经被某人更改了,然后询问他是否仍然要重写该数据还是首先检查已经更新的数据。(EF可以实现这种情况,在后面详细介绍)

d.拒绝修改:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,此时告诉该用户不允许更新该数据,因为数据已经被某人更新了。

(EF可以实现这种情况,在后面详细介绍)

②.消极并发(悲观并发、悲观锁):无论何时从数据库请求数据,数据都会被读取,然后该数据上就会加锁,因此没有人能访问该数据。这会降低并发出现问题的机会,缺点是加锁是一个昂贵的操作,会降低整个应用程序的性能。

 消极并发的本质就是永远不让冲突发生,通常的处理凡是是只读锁和更新锁。

a. 当把只读锁放到记录上时,应用程序只能读取该记录。如果应用程序要更新该记录,它必须获取到该记录上的更新锁。如果记录上加了只读锁,那么该记录仍然能够被想要只读锁的请求使用。然而,如果需要更新锁,该请求必须等到所有的只读锁释放。同样,如果记录上加了更新锁,那么其他的请求不能再在这个记录上加锁,该请求必须等到已存在的更新锁释放才能加锁。

总结,这里我们可以简单理解把并发业务部分用一个锁(如:lock,实质是数据库锁,后面章节单独介绍)锁住,使其同时只允许一个线程访问即可。

b. 加锁会带来很多弊端:

 (1):应用程序必须管理每个操作正在获取的所有锁;

 (2):加锁机制的内存需求会降低应用性能

 (3):多个请求互相等待需要的锁,会增加死锁的可能性。

总结:尽量不要使用消极并发,EF默认是不支持消极并发的!!!

注意:EF默认就是积极并发,当然EF也可以配置成消极并发。

二. 并发机制的解决方案

1. 从架构的角度去解决(大层次 如:12306买票)

  nginx负载均衡、数据库读写分离、多个业务服务器、多个数据库服务器、NoSQL, 使用队列来处理业务,将高并发的业务依次放到队列中,然后按照先进先出的原则, 逐个处理(队列的处理可以采用 Redis、RabbitMq等等)

  (PS:在后面的框架篇章里详细介绍该方案)

2. 从代码的角度去解决(在服务器能承载压力的情况下,并发访问同一条数据)

  实际的业务场景:如进销存类的项目,涉及到同一个物品的出库、入库、库存,我们都知道库存在数据库里对应了一条记录,入库要查出现在库存的数量,然后加上入库的数量,假设两个线程同时入库,假设查询出来的库存数量相同,但是更新库存数量在数据库层次上是有先后,最终就保留了后更新的数据,显然是不正确的,应该保留的是两次入库的数量和。

(该案例的实质:多个线程同时在一条相同的数据上执行多个数据库操作)

事先准备一张数据库表:

解决方案一:(最常用的方式)

  给入库和出库操作加一个锁,使其同时只允许一个线程访问,这样即使两个线程同时访问,但在代码层次上,由于锁的原因,还是有先有后的,这样就保证了入库操作的线程唯一性,当然库存量就不会出错了.

总结:该方案可以说是适合处理小范围的并发且锁内的业务执行不是很复杂。假设一万线程同时入库,每次入库要等2s,那么这一万个线程执行完成需要的总时间非常多,显然不适合。

    (这种方式的实质就是给核心业务加了个lock锁,这里就不做测试了)

 

解决方案二:EF处理积极并发带来的冲突

1. 配置准备

  (1). 针对DBFirst模式,可以给相应的表额外加一列RowVersion,数据库中为timestamp类型,对应的类中为byte[]类型,并且在Edmx模型上给该字段的并发模式设置为fixed(默认为None),这样该表中所有字段都监控并发。

如果不想监视所有列(在不添加RowVersion的情况下),只需在Edmx模型是给特定的字段的并发模式设置为fixed,这样只有被设置的字段被监测并发。

  测试结果: (DBFirst模式下的并发测试)

  事先在UserInfor1表中插入一条id、userName、userSex、userAge均为1的数据(清空数据)。

测试情况1:

  在不设置RowVersion并发模式为Fixed的情况下,两个线程修改不同字段(修改同一个字段一个道理),后执行的线程的结果覆盖前面的线程结果.

  发现测试结果为:1,1,男,1 ; 显然db1线程修改的结果被db2线程给覆盖了. (修改同一个字段一个道理)

 View Code

测试情况2:

  设置RowVersion并发模式为Fixed的情况下,两个线程修改不同字段(修改同一个字段一个道理),如果该条数据已经被修改,利用DbUpdateConcurrencyException可以捕获异常,进行积极并发的冲突处理。测试结果如下:

  a.RefreshMode.ClientWins: 1,1,男,1

  b.RefreshMode.StoreWins: 1,ypf,1,1

  c.ex.Entries.Single().Reload(); 1,ypf,1,1

复制代码

 1             {2                 //1.创建两个EF上下文,模拟代表两个线程3                 var db1 = new ConcurrentTestDBEntities();4                 var db2 = new ConcurrentTestDBEntities();5 6                 UserInfor1 user1 = db1.UserInfor1.Find("1");7                 UserInfor1 user2 = db2.UserInfor1.Find("1");8 9                 //2. 执行修改操作
10                 //(db1的线程先执行完修改操作,并保存)
11                 user1.userName = "ypf";
12                 db1.Entry(user1).State = EntityState.Modified;
13                 db1.SaveChanges();
14 
15                 //(db2的线程在db1线程修改完成后,执行修改操作)
16                 try
17                 {
18                     user2.userSex = "男";
19                     db2.Entry(user2).State = EntityState.Modified;
20                     db2.SaveChanges();
21 
22                     Console.WriteLine("测试成功");
23                 }
24                 catch (DbUpdateConcurrencyException ex)
25                 {
26                     Console.WriteLine("测试失败:" + ex.Message);
27 
28                     //1. 保留上下文中的现有数据(即最新,最后一次输入)
29                     //var oc = ((IObjectContextAdapter)db2).ObjectContext;
30                     //oc.Refresh(RefreshMode.ClientWins, user2);
31                     //oc.SaveChanges();
32 
33                     //2. 保留原始数据(即数据源中的数据代替当前上下文中的数据)
34                     //var oc = ((IObjectContextAdapter)db2).ObjectContext;
35                     //oc.Refresh(RefreshMode.StoreWins, user2);
36                     //oc.SaveChanges();
37 
38                     //3. 保留原始数据(而Reload处理也就是StoreWins,意味着放弃当前内存中的实体,重新到数据库中加载当前实体)
39                     ex.Entries.Single().Reload();
40                     db2.SaveChanges();
41                 }
42             }

复制代码

测试情况3:

  在不设置RowVersion并发模式为Fixed的情况下(也不需要RowVersion这个字段),单独设置userName字段的并发模式为Fixed,两个线程同时修改该字段,利用DbUpdateConcurrencyException可以捕获异常,进行积极并发的冲突处理,但如果是两个线程同时修改userName以外的字段,将不能捕获异常,将走EF默认的处理方式,后执行的覆盖先执行的。

  a.RefreshMode.ClientWins: 1,ypf2,1,1

  b.RefreshMode.StoreWins: 1,ypf,1,1

  c.ex.Entries.Single().Reload(); 1,ypf,1,1

 View Code

  (2). 针对CodeFirst模式,需要有这样的一个属性 public byte[] RowVersion { get; set; },并且给属性加上特性[Timestamp],这样该表中所有字段都监控并发。如果不想监视所有列(在不添加RowVersion的情况下),只需给特定的字段加上特性 [ConcurrencyCheck],这样只有被设置的字段被监测并发。

  除了再配置上不同于DBFirst模式以为,是通过加特性的方式来标记并发,其它捕获并发和积极并发的几类处理方式均同DBFirst模式相同。(这里不做测试了)

2. 积极并发处理的三种形式总结:

  利用DbUpdateConcurrencyException可以捕获异常,然后:

    a. RefreshMode.ClientWins:保留上下文中的现有数据(即最新,最后一次输入)

    b. RefreshMode.StoreWins:保留原始数据(即数据源中的数据代替当前上下文中的数据)

    c.ex.Entries.Single().Reload(); 保留原始数据(而Reload处理也就是StoreWins,意味着放弃当前内存中的实体,重新到数据库中加载当前实体)

3. 该方案总结:

  这种模式实质上就是获取异常告诉程序,让开发人员结合需求自己选择怎么处理,但这种模式是解决代码层次上的并发冲突,并不是解决大数量同时访问崩溃问题的。

解决方案三:利用队列来解决业务上的并发(架构层次上其实也是这种思路解决的)

1.先分析:

  前面说过所谓的高并发,就是海量的用户同时向服务器发送请求,进行某个业务处理(比如定时秒杀的抢单),而这个业务处理是需要 一定时间的。

2.处理思路:

  将海量用户的请求放到一个队列里(如:Queue),先不进行业务处理,然后另外一个服务器从线程中读取这个请求(MVC框架可以放到Global全局里),依次进行业务处理,至于处理完成后,是否需要告诉客户端,可以根据实际需求来定,如果需要的话(可以借助Socket、Signalr、推送等技术来进行).

  特别注意:读取队列的线程是一直在运行,只要队列中有数据,就给他拿出来.

  这里使用Queue队列,可以参考:http://www.cnblogs.com/yaopengfei/p/8322016.html

  (PS:架构层次上的处理方案无非队列是单独一台服务器,执行从队列读取的是另外一台业务服务器,处理思想是相同的)

队列单例类的代码:

 View Code

PS:这里的入队和出队都要加锁,因为Queue默认不是线程安全的,不加锁会存在资源竞用问题从而业务出错,或者直接使用ConcurrentQueue线程安全的队列,就不需要加锁了,关于队列线程安全问题详见:http://www.cnblogs.com/yaopengfei/p/8322016.html

临时存储数据类的代码:

复制代码

 1     /// <summary>2     /// 该类用来存储请求信息3     /// </summary>4     public class TempInfor5     {6         /// <summary>7         /// 用户编号8         /// </summary>9         public string userId { get; set; }
10     }

复制代码

模拟高并发入队,单独线程出队的代码:

复制代码

 1  {2                 //3.1 模拟高并发请求 写入队列3                 {4                     for (int i = 0; i < 100; i++)5                     {6                         Task.Run(() =>7                         {8                             TempInfor tempInfor = new TempInfor();9                             tempInfor.userId = Guid.NewGuid().ToString("N");
10                             //下面进行入队操作
11                             QueueUtils.instanse.Enqueue(tempInfor);
12 
13                         });
14                     }
15                 }        
16                 //3.2 模拟另外一个线程队列中读取数据请求标记,进行相应的业务处理(该线程一直运行,不停止)
17                 Task.Run(() =>
18                 {
19                     while (true)
20                     {
21                         if (QueueUtils.instanse.getCount() > 0)
22                         {
23                             //下面进行出队操作
24                             TempInfor tempInfor2 = (TempInfor)QueueUtils.instanse.Dequeue();
25 
26                             //拿到请求标记,进行相应的业务处理
27                             Console.WriteLine("id={0}的业务执行成功", tempInfor2.userId);
28                         }
29                     }           
30                 });
31                 //3.3 模拟过了一段时间(6s后),又有新的请求写入
32                 Thread.Sleep(6000);
33                 Console.WriteLine("6s的时间已经过去了");
34                 {
35                     for (int j = 0; j < 100; j++)
36                     {
37                         Task.Run(() =>
38                         {
39                             TempInfor tempInfor = new TempInfor();
40                             tempInfor.userId = Guid.NewGuid().ToString("N");
41                             //下面进行入队操作
42                             QueueUtils.instanse.Enqueue(tempInfor);
43 
44                         });
45                     }
46                 }
47             }

复制代码

3.下面案例的测试结果:

  一次输出100条数据,6s过后,再一次输出100条数据。

4. 总结:

  该方案是一种迂回的方式处理高并发,在业内这种思想也是非常常见,但该方案也有一个弊端,客户端请求的实时性很难保证,或者即使要保证(比如引入实时通讯技术),

 也要付出不少代价.

 

解决方案四: 利用数据库自有的锁机制进行处理

   (在后面数据锁机制章节进行介绍)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438499.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二十一节:ADO层次上的海量数据处理方案(SqlBulkCopy类插入和更新)

一. 简介 1. 背景&#xff1a; 虽然前面EF的扩展插件Z.EntityFramework.Extensions&#xff0c;性能很快&#xff0c;而且也很方便&#xff0c;但是该插件要收费&#xff0c;使用免费版本的话&#xff0c;需要定期更新&#xff0c;如果不更新&#xff0c;将失效&#xff0c;非…

第二十二节: 以SQLServer为例介绍数据库自有的锁机制(共享锁、更新锁、排它锁等)和事务隔离级别 :

一. 基本概念 1.共享锁&#xff1a;(holdlock) (1). select的时候会自动加上共享锁&#xff0c;该条语句执行完&#xff0c;共享锁立即释放&#xff0c;与事务是否提交没有关系。 (2). 显式通过添加(holdlock)来显式添加共享锁&#xff08;比如给select语句显式添加共享锁&…

r语言中1c0怎么表示什么,r语言表示或者用什么符号?

犯罪嫌疑人X1、对象名称中的句点(.)没有特殊意义。但美元符号($)却有着和其他语言中的句点类似的含义&#xff0c;即指定一个对象中的某些部分例如&#xff1a;(1)A$x是指数据框A中的变量x。(2)lm.x 指一个变量&#xff0c;lm$x则指对象lm的一个属性。2、 R不提供多行注释或块注…

第二十三节: EF性能篇(三)之基于开源组件 Z.EntityFrameWork.Plus.EF6解决EF性能问题

一. 开篇说明 EF的性能问题一直以来经常被人所吐槽&#xff0c;究其原因在于“复杂的操作在生成SQL阶段耗时长&#xff0c;且执行效率不高”&#xff0c;但并不是没有办法解决&#xff0c;从EF本身举几个简单的优化例子&#xff1a; ①&#xff1a;如果仅是查询数据&#xff0c…

c语言怎样计算栈的长度,数据结构与算法:栈 C语言实现

栈是仅在表尾进行插入、删除操作的线性表。即栈 S (a1, a2, a3, ………,an-1, an)&#xff0c;其中表尾称为栈顶 /top&#xff0c;表头称为栈底/base。由于只能在表尾进行操作&#xff0c;因此栈的运算规则就是“后进先出”(LIFO)和线性表类似&#xff0c;栈也有两种存储结构—…

WebApi系列(从.Net 到 .Net Core)【更新】

一. 简介 1. 什么是WebApi&#xff1f; WebApi是一个很广泛的概念&#xff0c;在这里我们特指.Net平台下的Asp.Net WebApi框架&#xff0c;它是针对各种客户端(浏览器、APP等)来构建Http服务的一个框架&#xff0c;它是一种RestFul风格的开发接口的技术&#xff0c;它比WebServ…

第一节:WebApi的纯原生态的RestFul风格接口和路由规则介绍

一. 原生态接口 1. 从默认路由开始分析 在WebApiConfig.cs类中的Register方法中&#xff0c;我们可以看到默认路由如下&#xff1a; 分析&#xff1a;请求地址在 controller 前面需要加上 api/&#xff0c;controller后面没有action&#xff0c;最后有一个参数{id}&#xff0c…

第二节:如何正确使用WebApi和使用过程中的一些坑

一. 基本调用规则 1. 前提 WebApi的默认路由规则为&#xff1a;routeTemplate: "api/{controller}/{id}", 下面为我们统一将它改为 routeTemplate: "api/{controller}/{action}/{id}",这样我们在调用的时候&#xff0c;还是通过拼接方法名来识别&#xff0…

第三节:总结.Net下后端的几种请求方式(WebClient、WebRequest、HttpClient)

一. 前言 前端调用有Form表单提交&#xff0c;ajax提交&#xff0c;ajax一般是用Jquery的简化写法&#xff0c;在这里不再过多介绍&#xff1b; 后端调用大约有这些&#xff1a;WebCient、WebRequest、Httpclient、WebapiClient&#xff0c;重点探讨Get和Post请求&#xff0c;P…

android 传感器ceshi,Android代码-传感器-测试手机支持那几种传感

Android代码----传感器-----测试手机支持那几种传感一个小小Demo检测手机支持那几种传感&#xff1a;具体代码如下&#xff1a;[Java代码]DemoSensorActivity.javapackage com.example.testsensor;import java.util.List;import android.app.Activity;import android.content.C…

第四节:跨域请求的解决方案和WebApi特有的处理方式

一. 简介 前言&#xff1a; 跨域问题发生在Javascript发起Ajax调用&#xff0c;其根本原因是因为浏览器对于这种请求&#xff0c;所给予的权限是较低的&#xff0c;通常只允许调用本域中的资源&#xff0c; 除非目标服务器明确地告知它允许跨域调用。假设我们页面或者应用已在 …

手机存储android文件怎么打开,安卓手机如何打开.jio文件?

01安卓手机无法打开.jio文件&#xff0c;需要在电脑上安装久其通用数据管理平台软件打开。进入软件首页&#xff0c;点击菜单栏“装入”按钮&#xff0c;弹出“数据装入向然后导”&#xff0c;单击文件夹图标&#xff0c;在弹出“打开”窗口中选择装入数据的路径&#xff0c;单…

第五节:WebApi的三大过滤器

一. 基本说明 1. 简介&#xff1a; WebApi下的过滤器和MVC下的过滤器有一些区别,首先我们要注意的是通常建WebApi项目时&#xff0c;会自动把MVC的程序集也引入进来&#xff0c;所以我们在使用WebApi下的过滤器的时候&#xff0c;要引入“ System.Web.Http”这个程序集&#x…

android动态贴纸实现原理,人脸动态贴纸sdk算法详解,人脸动态贴纸功能如何实现...

原标题&#xff1a;人脸动态贴纸sdk算法详解&#xff0c;人脸动态贴纸功能如何实现泛娱乐行业在互联网领域中发展趋势逐渐增强&#xff0c;而直播、短视频、视频社交等作为头部产品受到了众多用户的关注和喜爱。为了能够更好的满足用户体验&#xff0c;众多APP纷纷开始接入人脸…

第六节:WebApi的部署方式(自托管)

一. 简单说明 开篇就介绍过WebApi和MVC相比&#xff0c;其中优势之一就是WebApi可以不依赖于IIS部署&#xff0c;可以自托管&#xff0c;当然这里指的是 .Net FrameWork 下的 WebApi 和 MVC 相比较&#xff0c;在.Net Core下&#xff0c;当然就另行别论。 下面我们重点介绍的就…

第七节:WebApi与Unity整合进行依赖注入和AOP的实现

一. IOC和DI 1. 通过Nuget引入Unity程序集。 PS:【版本&#xff1a;5.8.6】 2. 新建DIFactory类&#xff0c;用来读取Unity的配置文件并创建Unity容器&#xff0c;需要注意的是DIFactory类需要声明成单例。 PS&#xff1a;这里采用静态构造函数(必须是无参的)的形式来实现单…

第八节:常见安全隐患和传统的基于Session和Token的安全校验

一. 常见的安全隐患 1. SQL注入 常见的案例&#xff1a; String query "SELECT * FROM T_User WHERE userID" Request["userID"] "; 这个时候&#xff0c;只需要在传递过来的userID后面加上个&#xff1a; or 11&#xff0c;即可以获取T_User表中…

android手机设置时间设置,如何设置电信定制手机日期与时间

使用电信定制手机如A765e、A600e、A560e等的时候&#xff0c;可能会发现手机的日期和时间没法自己设置。这主要是因为手机使用电信卡时&#xff0c;会自动与电信基站进行交互并自动调节日期与时间。所以不论使用电信定制的单模或双模手机(单模是指手机只有一个卡槽&#xff0c;…

第二节 CSS入门介绍

一.背景 这里将陆续介绍前端CSS中相关知识&#xff0c;先介绍CSS2.1&#xff0c;后续会介绍CSS3的相关属性&#xff0c;通过该系列的文章&#xff0c;希望能给准备转战前端的人员一些帮助&#xff0c;同时也帮助自己梳理知识&#xff0c;文章中如有错误&#xff0c;欢迎指出。 …

三星sec.android.soagent,3.0降级2.5教程

给小白看的。下载五件套&#xff0c;odia&#xff0c;驱动&#xff0c;地址&#xff1a;http://www.samsungmembers.cn/thread-1019962-110-150.html&#xff0c;或者自己论坛搜索&#xff0c;请下载G9810ZCU2BTJA&#xff0c;别下k3最后一个版本的会出问题&#xff0c;刷机后再…