作者 | 羽生结弦

责编 | 胡雪蕊

出品 | CSDN(ID: CSDNnews)

在大多数的应用中都会出现客户端同时发送多个请求对同一条数据就行修改,这个时候就会出现并发冲突。我们一般的做法会有如下两种:

1. 乐观并发所谓的乐观并发就是多个请求同时对同一条数据的更新,只有最后一个更新请求会被保存,其他更新请求将会被抛弃。

2. 悲观并发所谓悲观并发就是多个请求同时对同一条数据的更新,只有当前更新请求完成或者被抛弃,才会执行下一个更新请求,如果当前更新请求未完成或者未被抛弃,那么后面所有的更新请求将会被阻塞。

通过上面的简单讲解我们简单的了解了如何处理并发请求,那么下面我们来看一下上面两种做法的具体讲解和实现。

方法一

在 Entity Framework 中,默认的解决方案是乐观并发,原因是当出现并发情况的时候,内部没有任何对其他客户端访问同一行数据的限制。我们来看一下例子,我们在数据库中存有一条数据,数据如下图所示:下面我们来修改一下 Name 字段的值:

csharpclass Program{    static void Main(string[] args){        int userId = 1;        using (var db = new EfContext())        {            using (var ef = new EfContext())            {                User user1 = db.Users.FirstOrDefault(p => p.Id == userId);                User user2= ef.Users.FirstOrDefault(p => p.Id == userId);                user1.Name = "李四";                db.SaveChanges();                user2.Name = "王五";                ef.SaveChanges();            }        }    }}

在上面的代码中我们利用嵌套 using 的形式实现了并发访问。首先我们同时查询出 id 等于1的人员,然后将 user1 中的 Name 修改为李四并提交,接着再把 user2 中的 Name 修改为王五并提交。这个时候我们再查询数据库就会发现 Name 列被更新为了最后一次提交值王五,如下图所示:

上述操作发生了什么呢?我们来看一下,首先我们利用 db 从数据库中读取了 id 等于1的人员信息,此时该人员信息为张三,然后我们将 Name 值改为李四,并且提交到了数据库,在这个时候,数据库中的Name值将不再是张三,而是李四。接着我们再将 user2 的 Name 值修改为王五,并提交的数据库,这个时候数据库的 Name 列的值变为了王五。上述情况下,Entity Framework 将修改转换为 update 语句时是利用主键来定位指定行,因此上面两次操作都会成功,只不过最后一次修改的数据会最终持久化到数据库中。但是这种方式存在一个巨大的隐患,例如在门票预售系统中,门票的数量是有限制的,购票人数超过门票数量限制将会禁止购买。如果利用 Entity Framework 默认的乐观并发模式,每次有并发请求购票时,每个请求都会减去门票数量,并且向数据库中插入一条购票信息,这样一来永远是最后一个请求的数据会持久化到数据库中,这样就造成了门票预约人数超过了门票的限制数量。

针对上面所说的问题,我么可以利用如下两种方式来解决:

1. 并发 Token

利用这个方法我们只需在实体类对应的 Map 文件的构造函数中加让类似下面的代码即可:

csharpProperty(p => p.Name).IsConcurrencyToken();

2. 行版本

通过行版本设置,我们需要为实体添加一个行版本子字节数组,代码如下:

csharppublic byte[] RowVersion { get; set; }

然后将行版本字段映射进数据库,这样每次更新数据的时候都行版本字段也会跟着更新。最后我们在实体类对应的 Map 文件的构造函数中添加如下代码即可:

csharpProperty(p => p.RowVersion).IsRowVersion();

这样在每次提交修改请求时 Entity Framework 都会检查数据库中的行版本和当前提交数据的行版本是否一致,如果一直就更新数据和行版本信息。上述两种方法都将会引发并发异常,那么我们该如何解决这个异常呢?我们需要用到并发异常类( DbUpdateConcurrencyException )中的 Entries 属性,该属性是一个集合。我们需要调用集合中每个对象的 Reload 方法将数据库中最新的值放在内存中。这样后续的实体值将和数据库保持一致。完成这一步后,我们可以重新向数据库提交更新数据。具体实现代码如下:

csharpclass Program{static void Main(string[] args){int userId = 1;using (var db = new EfContext())        {using (var ef = new EfContext())            {                User user1 = db.Users.FirstOrDefault(p => p.Id == userId);                User user2= ef.Users.FirstOrDefault(p => p.Id == userId);                user1.Name = "李四";                db.SaveChanges();try                {                    user2.Name = "王五";                    ef.SaveChanges();                }catch (DbUpdateConcurrencyException e)                {foreach (var item in e.Entries)                    {                        item.Reload();                        ef.SaveChanges();                    }                }            }        }    }}

这里需要注意的是这个方法并不是万能的,只是将当前客户端的值成功存入数据库中,这种情况被称为客户端获胜,当然了还有数据库获胜,以及数据库和客户端合并获胜(这三个概念解决并发冲突的方式将在下一小节讲解)。在讲解这个问题前我们先来了解一下 Entity Framework 的原始值和更新后的数据库值以及当前值从哪里获得。代码如下:

csharptry{  //more code}catch (DbUpdateConcurrencyException e){    foreach (var item in e.Entries)    {        //原始值        var ov = item.OriginalValues.ToObject();        //更新后数据库值        var dv = item.GetDatabaseValues().ToObject();        // 当前值        var nv = item.CurrentValues.ToObject();    }}

从上面的代码中我们可以看到获取这三种值我们依然是从并发异常类的 Entries 属性中获得。看到这里一定会有人想到不利用 Reload 方法来更新内存中的最新值,而是直接利数据库值更新当前内存中的值,如果你想到这里说明你已经掌握了解决并发冲突最简单的方法。那么我们就来看一下代码:

csharptry{    //more code}catch (DbUpdateConcurrencyException e){    foreach (var item in e.Entries)    {        Object dv = item.GetDatabaseValues().ToObject();        item.OriginalValues.SetValues(dv);        ef.SaveChanges();    }}

方法二

上一小节中我们提到了客户端获胜、数据库获胜以及数据库和客户端合并获胜,并且讲解了原始值和更新后的数据库值以及当前值从哪里获得的。在这一节将利用客户端获胜、数据库获胜以及客户端和数据库合并获胜处理并发的方法。

1. 客户端获胜

当调用 SaveChanges 方法时,如果存在并发冲突将会引发 DbUpdateConcurrencyException 异常,那么这个时候我们将调用 handleDbUpdateConcurrencyException 函数来处理异常并正确解决冲突,最后在调用 SaveChanges 方法重试提交数据。如果依然排除 DbUpdateConcurrencyException 异常,将不在进行处理。我们来看以下代码:

csharpclass Program{    static void Main(string[] args){        int userId = 1;        using (var db = new EfContext())        {            using (var ef = new EfContext())            {                User user1 = db.Users.FirstOrDefault(p => p.Id == userId);                User user2 = ef.Users.FirstOrDefault(p => p.Id == userId);                user1.Name = "李四";                db.SaveChanges();                try                {                    user2.Name = "王五";                    ef.SaveChanges();                }                catch (DbUpdateConcurrencyException e)                {                    Retry(ef, handleDbUpdateConcurrencyException: exception =>                    {                        exception = (e as DbUpdateConcurrencyException).Entries;                        foreach (var item in exception)                        {                            item.OriginalValues.                                SetValues(item.GetDatabaseValues());                        }                    });                }            }        }    }}

上述代码中发生并发异常时,将会将数据库的值提交到内存中,然后重新提交更新数据。2. 数据库获胜如果你想让数据库获胜,那就简单了。再发生异常时不需做任何处理,只返回方法的返回值即可。我们将上一个例子的代码更新一下:

csharpclass Program{    static void Main(string[] args){        int userId = 1;        using (var db = new EfContext())        {            using (var ef = new EfContext())            {                User user1 = db.Users.FirstOrDefault(p => p.Id == userId);                User user2 = ef.Users.FirstOrDefault(p => p.Id == userId);                user1.Name = "李四";                db.SaveChanges();                try                {                    user2.Name = "王五";                    ef.SaveChanges();                }                catch (DbUpdateConcurrencyException e)                {                    return;                }            }        }    }}

上面代码运行后,只有李四会被更新到数据库中,王五因为并发冲突且异常捕获后没有进行任何处理而不会存入数据库。

3. 数据库和客户端合并获胜

这种方式是最复杂的,需要合并数据库和客户端的数据,如果用到此方法我们需要谨记如下两点:

  • 如果原始值与数据库中的值不通,就说明数据库中的值已经被其他客户端更新,这时必须放弃当前的更新,保留数据库的更新;

  • 如果原始值与数据库的值相同,代表不会发生并发冲突,按照正常处理流程处理即可。

同样,我们将上面的例子按照上面两点进行修改:

csharpclass Program{    static void Main(string[] args)    {        int userId = 1;        using (var db = new EfContext())        {            using (var ef = new EfContext())            {                User user1 = db.Users.FirstOrDefault(p => p.Id == userId);                User user2 = ef.Users.FirstOrDefault(p => p.Id == userId);                user1.Name = "李四";                db.SaveChanges();                try                {                    user2.Name = "王五";                    ef.SaveChanges();                }                catch (DbUpdateConcurrencyException e)                {                    Retry(ef, handleDbUpdateConcurrencyException: exception =>                    {                        exception = (e as DbUpdateConcurrencyException).Entries;                        foreach (var item in exception)                        {                            Object dv = item.GetDatabaseValues();                            Object ov = item.OriginalValues();                            item.OriginalValues.SetValues(dv);                            dv.PropertyNames.Where(property =>                                !object.Equals(ov[property], dv[property])).ToList().ForEach(property =>                                item.Property(property).IsModified = false);                        }                    });                }            }        }    }}

方法三

前面两种方法都是利用 SaveChanges 捕获并发异常,其实我们也可以自定义 SaveChanges 的扩展方法来处理并发异常。下面我们就来看一下具体的两种策略。

1. 普通策略

这个策略非常简单,就是利用循环来实现重试机制,代码如下:

csharppublic static partial class DbContextExtensions{    public static int SaveChanges(this DbContext dbContext, Action> action,int retryCount = 3){        if (retryCount <= 0)        {            throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0");        }        for (int retry=1;retry        {            try            {            }            catch (DbUpdateConcurrencyException e) when (retry             {                resolveConficts(e.Entries);            }        }        return dbContext.SaveChanges();    }}

2. 高级策略

在 .NET 中已经有开发人员帮我们开发出了强大的工具 Polly ,Polly 是一个 .NET 弹性和瞬态故障处理库,允许开发人员以 Fluent 和线程安全的方式来实现重试、断路、超时、隔离和回退策略。

  • 首先我们需要定义一个枚举类型

csharppublic enum RefreshConflict{    StoreWins,    ClientWins,    MergeClientAndStore}

  • 然后根据不同的获胜模式来刷新数据库的值

csharppublic static class RefreshEFStateExtensions{public static EntityEntry Refresh(this EntityEntry tracking,        RefreshConflict refreshMode){switch (refreshMode)        {case RefreshConflict.StoreWins:            {                tracking.Reload();break;            }case RefreshConflict.ClientWins:            {                PropertyValues databaseValues = tracking.GetDatabaseValues();if (databaseValues == null)                {                    tracking.State = EntityState.Detached;                }else                {                    tracking.OriginalValues.SetValues(databaseValues);                }break;            }case RefreshConflict.MergeClientAndStore:            {                PropertyValues databaseValues = tracking.GetDatabaseValues();if (databaseValues == null)                {                    tracking.State = EntityState.Detached;                }else                {//当实体被更新时,刷新数据库原始值                    PropertyValues originalValues = tracking.OriginalValues.Clone();                    tracking.OriginalValues.SetValues(databaseValues);//如果数据库中对于属性有不同的值保留数据库中的值#if SelfDefine                      databaseValues.PropertyNames // Navigation properties are not included.                          .Where(property => !object.Equals(originalValues[property], databaseValues[property]))                          .ForEach(property => tracking.Property(property).IsModified = false);#else                        databaseValues.Properties                            .Where(property => !object.Equals(originalValues[property.Name],                                databaseValues[property.Name]))                            .ToList()                            .ForEach(property =>                                tracking.Property(property.Name).IsModified = false);#endif                }break;            }        }return tracking;    }}
  • 最后定义刷新状态的方法

csharppublic static partial class DbContextExtensions{    public static int SaveChanges(this DbContext context, RefreshConflict refreshMode, int retryCount = 3){        if (retryCount <= 0)        {            throw new ArgumentOutOfRangeException(nameof(retryCount), $"{retryCount}必须大于0");        }        return context.SaveChanges(        conflicts => conflicts.ToList().ForEach(tracking => tracking.Refresh(refreshMode)), retryCount);    }    public static int SaveChanges(this DbContext context, RefreshConflict refreshMode, RetryStrategy retryStrategy) =>            context.SaveChanges(                conflicts => conflicts.ToList().ForEach(tracking => tracking.Refresh(refreshMode)), retryStrategy);}

到这里 Entity Framework 解决并发冲突的方案已经讲完了,上面这几种方案都是固定的写法,大家可以直接将上面的代码复制进项目中使用。作者简介:朱钢,笔名羽生结弦,CSDN博客专家,.NET高级开发工程师,7年一线开发经验,参与过电子政务系统和AI客服系统的开发,以及互联网招聘网站的架构设计,目前就职于北京恒创融慧科技发展有限公司,从事企业级安全监控系统的开发。

【END】

 热 文 推 荐 

☞@程序员,一文掌握 Web 应用中的图片优化技巧!

☞Google 搜索点击量不到 50%?

☞马云马斯克激辩:AI 是威胁还是被低估了?

☞程序员易踩的 9 大坑!

☞99年少年12岁时买下100枚比特币, 如今却将所有积蓄压在一个不知名的代币上,还放话将超越Libra!

☞强推!阿里数据科学家一次讲透数据中台

☞冠军奖3万元!CSDN×易观算法大赛开赛啦

☞可惜了,你们只看到“双马会”大型尬聊

☞如何写出让同事无法维护的代码?

你点的每个“在看”,我都认真当成了喜欢

scanf_s 发送访问冲突_程序员如何解决并发冲突的难题?相关推荐

  1. 程序员如何解决并发冲突的难题?

    作者 | 羽生结弦 责编 | 胡雪蕊 出品 | CSDN(ID: CSDNnews) 在大多数的应用中都会出现客户端同时发送多个请求对同一条数据就行修改,这个时候就会出现并发冲突.我们一般的做法会有如 ...

  2. 从程序员角度--解决ipad白苹果问题的最佳办法---纠正网上的错误言论

    从程序员角度--解决ipad白苹果问题的最佳办法---纠正网上的错误言论 参考文章: (1)从程序员角度--解决ipad白苹果问题的最佳办法---纠正网上的错误言论 (2)https://www.cn ...

  3. 【源码+演示】高考加油!HTML+CSS特效文字祝福_程序员祝福高考学子旗开得胜!

    又是一年盛夏至,愿高三学子高中毕业日,即是高中名校时! 本篇为大家带来3款HTML+CSS制作出的小项目,为高考学子加油助威,愿他们旗开得胜,金榜题名![源码+演示]高考加油!HTML+CSS特效文字 ...

  4. 怎么查电脑系统版本_程序员的bug解决不了怎么办?

    一开始公司新来的新手程序员,最近已经开始了他们的工作,部门的项目经理,为了让他们快速上手,让他们对之前的系统进行BUG修复工作,我认为,这种思路对新手程序员来说,是很有益的,通过查找解决BUG,来熟悉 ...

  5. 程序员职业技能编写_程序员不需要的不需要编写代码的技能

    程序员职业技能编写 You can build the best application in the world, but if you don't know how to tell anyone ...

  6. 电脑怎么分屏2个显示器_程序员一台电脑装2个显示屏?因为专业

    使用电脑双屏显示已经十分普及,并不一定只有电影里面可以看到. 笔记本通过显示器输出.电脑通过投影输出.单个主机通过两台显示输出,均属于双屏显示. 当然,双频显示可以极大的提高工作效率. 以程序员为例, ...

  7. 参数调优为什么要采样_程序员精进之路:性能调优利器--火焰图

    本文主要分享火焰图使用技巧,介绍 systemtap 的原理机制,如何使用火焰图快速定位性能问题原因,同时加深对 systemtap 的理解. 让我们回想一下,曾经作为编程新手的我们是如何调优程序的? ...

  8. u盘锁电脑_程序员用U盘偷公司源码被抓;Rust 语言发布 5 周年

    (给技术最前线加星标,每天看技术热点) 综合整理:技术最前线(ID:TopITNews)参考:程序员的那些事.开源中国.solidot.cnBeta.腾讯科技等 0.程序员用U盘偷公司源码被判刑 20 ...

  9. webservice入参是一个对象_程序员技术精进:面向对象与服务的分析与设计

    面向对象分析与设计 面向对象分析与设计是指根据面向对象方法学对软件系统进行分析与设计.在面向对象分析与设计的定义中有三个关键词:面向对象.分析.设计.所以,为了更好地理解面向对象分析与设计的作用,我们 ...

最新文章

  1. 后端码农谈前端(CSS篇)第一课:CSS概述
  2. shoot for用法
  3. connectionstring mysql_Entity Framework 6 自定义连接字符串ConnectionString连接MySQL
  4. python-socket2
  5. 写表单验证等页面的总结
  6. 005-统一沟通-部署-基础-环境作业
  7. Microsoft visio 2010 Premium 的激活
  8. 2017华为面试算法题小结
  9. lisp 角平分线_最佳Visual LISP及VBA for AutoCAD2000程序123例
  10. 2022Android高级面试题汇总解答,2022-2022阿里巴巴安卓面试真题解析
  11. Redisson的看门狗机制
  12. 磁盘显示无法访问数据错误循环冗余检查的资料恢复法子
  13. Digging Into Self-Supervised Monocular Depth Estimation(2019.8)
  14. Java程序员工作三年以内
  15. linux主板上的网卡不显示,redhat4.8在MSI主板安装完识别不到网卡RTL8168
  16. 天桥脑科学研究院推出“对话大脑”院士论坛:国际大咖畅谈脑机接口
  17. crm客户关系管理系统总结
  18. 黑马程序员-logback.xml配置文件
  19. 日本高铁想到的网络安全
  20. Microsoft Visual Studio 13强力卸载

热门文章

  1. CVPR 2020 《Transform and Tell: Entity-Aware News Image Captioning》论文笔记(数据集)
  2. ICCV 2017 《Towards End-to-End Text Spotting with Convolutional Recurrent Neural Network》论文笔记
  3. 2019-11-08 频域的直观感受
  4. [导入]给家人补补钙!双莲炖腔骨
  5. 3-无重复字符的最长子串(中等)
  6. Express框架Restful API Ajax 跨域 开启Cookie支持
  7. Django中重定向页面的时候使用命名空间
  8. crontab -e
  9. Eclipse 批量创建多级文件夹
  10. Apex 的异常处理