首页 > 其他分享 >EFCore 高并发

EFCore 高并发

时间:2024-04-10 18:00:13浏览次数:25  
标签:处理 数据库 并发 int EFCore var public

EFCore 高并发

有常见的并发场景?如果我们使用EFCore常用的解决方法会出现哪些问题?对应不同的并发场景我们应该选择哪些的处理方式?

参照:事务的四种隔离级别详解_事务隔离级别-CSDN博客

什么是高并发

高并发意味着大流量,需要运用技术手段抵抗流量的冲击,这些手段好比操作流量,能让流量更平稳地被系统所处理,带给用户更好的体验。

常见的高并发场景

  1. Web服务器处理请求 :Web服务器需要同时处理多个来自不同用户的请求,这些请求可能包括静态资源请求、动态页面生成、数据库查询等。
  2. 多用户在线游戏 :在线游戏需要同时处理多个玩家的操作,例如移动、攻击、聊天等,而这些操作都需要即时响应。
  3. 多线程文件处理 :一个文件处理程序可以同时处理多个文件,每个文件可能在不同的线程中处理,以提高处理效率。
  4. 实时数据分析 :在大数据环境下,需要对实时数据进行分析和处理,这就需要并发地处理多个数据流。
  5. 操作系统资源管理 :操作系统需要同时管理多个应用程序和进程的资源请求,如CPU时间、内存、磁盘IO等。
  6. 电子商务网站订单处理 :电子商务网站需要同时处理多个用户提交的订单,包括库存管理、支付处理、发货等操作。
  7. 多线程网络编程 :网络服务器需要同时处理多个客户端的连接请求和数据交换,以提供并发的网络服务。
  8. 批处理任务 :某些任务需要同时处理大量的数据,如数据清洗、转换、分析等,可以通过并行处理来提高效率。
  9. 实时通信应用 :即时通讯应用程序需要支持多个用户之间的实时消息传递,这就需要并发处理消息发送和接收。
  10. 科学计算 :在科学计算领域,需要对大规模数据进行并行处理,以加速计算过程,如天气预测、分子模拟等。

具体到数据库层面的场景,高并发意味着单位时间内要处理大量的数据的增、删、改等操作,保障正确的同时保证数据能正常可读。

EFCore处理高并发

现主要存在两种处理方式:悲观并发、乐观并发

概念介绍

悲观并发

悲观并发每次访问资源时都上锁,使得其它用户无法访问该资源。这种策略就是假设用户每次访问资源都认定会更新资源,所以每次访问资源就上锁,当资源被修改时,同一资源上的所有其它并发操作都将被暂停,直到当前操作完成,并且该资源上的锁被释放。

这种方法在资源争用较高的场景中是一个不错的选择。可以在锁定持续时间较短的场景中利用悲观并发。但是,当用户与数据交互时,悲观并发不能很好的扩展,会导致资源锁住很长的时间。再者当互联网连接较弱或断开连接时,将无法正确管理数据,这会影响数据库的工作。

乐观并发

乐观并发假设用户每次访问资源都不会更新资源,所以资源不会被锁定。但是在更新的时候会判断在此期间其它用户是否有更新操作。

乐观并发遵循“保存最新数据”的策略。

乐观并发通常用于数据稀缺的场景,也就是常说的频繁读取,这样可以提高吞吐量,但也会消耗额外的服务器资源 。

乐观并发提高了性能也更好的支持扩展,因为它允许服务器在更短时间内为更多的客户端提供服务。

两种方式对比

悲观并发:比如有两个用户A,B,同时登录系统修改一个文档,如果A先进入修改,则系统会把该文档锁住,B就没办法打开了, 只有等A修改完,完全退出的时候B才能进入修改。

乐观并发:A,B两个用户同时登录,如果A先进入修改紧跟着B也进入了。A修改文档的同时B也在修改,如果在A保存之后, B再保存他的修改,此时系统检测到数据库中文档记录与B刚进入时不一致,B保存时会抛出异常,修改失败。

具体解决方案

具体对应的操作:悲观并发—事务;乐观并发—乐观锁

EF Core支持监控乐观并发的两种方案:监测单个字段和监测整条数据,每种都对应DataAnnotations 和 FluentApi的两种配置方式,任选其一就行。

一种是将实体配置为并发令牌(监测单个字段);另一种是在实体类中添加行版本属性(监测整行数据)。

并发令牌

Data Annotation 配置方式

 /// <summary>
    /// 手机库存
    /// </summary>
    public class PhoneStock
    {
        public int Id { get; set; }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 价格
        /// </summary>
        public Decimal Price { get; set; }
   
        /// <summary>
        /// 数量
        /// </summary>
        public int Amount { get; set; }
        /// <summary>
        /// 版本
        /// </summary>
        /// 
        [ConcurrencyCheck]
        public Guid RowVer { get; set; }
    }

Fluent API 配置方式

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<PhoneStock>()
        .Property(p => p.RowVer)
        .IsConcurrencyToken();
}

两种配置方式选择一种即可,使用并发令牌 EF Core 不会自动生成值,我们在更新实体记得给 RowVer属性赋值。

对应涉及高一致性字段对应的类,我们可以在对应的DBContext里重写SaveChange方法,在保存前给RowVer赋新值,这样不用写大量的并发令牌对应的赋值逻辑

public override int SaveChanges()
        {
            //给版本号赋值
            //检查数据库更改
            this.ChangeTracker.DetectChanges();

            //筛选新增/修改的实体对象
            var modifiedEntities = this.ChangeTracker
                .Entries()
                .Where(x => x.State == EntityState.Modified || x.State == EntityState.Added)
                .Select(x => x.Entity)
                .ToList();
            foreach (var entity in modifiedEntities)
            {
                //存储一个新的Guid值
                var property = entity?.GetType().GetProperty("RowVer");
                property?.SetValue(entity, Guid.NewGuid());
            }
            return base.SaveChanges();
        }

行版本属性

Data Annotation 配置方式

public class Roles
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Fluent API 配置方式

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Roles>()
        .Property(p => p.Timestamp)
        .IsRowVersion();
}

也是两种选择一种,但与并发令牌不同的是不用给 Timestamp 赋值,EF Core 会自动产生一个值。

!!!注意,这种方式只在数据库sqlserver使用时有效,Timestamp能自动生成值,所以如果使用的是mysql还是选择第一种并发令牌的方式

并发异常处理(mysql建议使用并发令牌)

参照:处理并发冲突 - EF Core | Microsoft Learn

配置完之后 ,实体执行更新或删除操作时,EF Core 都会将请求中的信息值与数据库表中的值进行比较。

  • 如果两个值匹配,则执行操作。
  • 如果值不匹配,则更新或删除操作将中止并且抛出一个 DbUpdateConcurrencyException。

当 EF Core 抛出 DbUpdateConcurrencyException ,我们可以做如下选择进行处理:

  • 中止操作并要求用户刷新界面重新操作,称为使用他的,但会覆盖本地的操作数据。
  • 使用当前值(最新一次提交的数据)并尝试重新 SaveChanges ,称为使用我的,但会覆盖服务器上的数据。
  • 将冲突记录下来,让用户自己选择使用那个数据,称为手动解决。这种方式始终保持数据最新。

有三组值可用于帮助解决并发冲突:

  • “当前值”是应用程序尝试写入数据库的值。
  • “原始值”是在进行任何编辑之前最初从数据库中检索的值。
  • “数据库值”是当前存储在数据库中的值。

处理并发冲突的常规方法是:

  1. SaveChanges 期间捕获 DbUpdateConcurrencyException
  2. 使用 DbUpdateConcurrencyException.Entries 为受影响的实体准备一组新更改。
  3. 刷新并发令牌的原始值以反映数据库中的当前值。
  4. 重试该过程,直到不发生任何冲突
 /// <summary>
        /// 售出扣减库存
        /// </summary>
        /// <param name="id"></param>
        /// <param name="amount"></param>
        /// <returns></returns>
        public async Task<bool> PurchasePhone(int id, int amount)
        {
            var stock = await dbContext.Phones.SingleOrDefaultAsync(s => s.Id == id);
            int changeCount = 0;
            if (stock != null && stock.Amount >= amount)
            {
                stock.Amount -= amount;
                //dbContext.Update(stock);
                ///异常处理
                var saved = false;
                while (!saved)
                {
                    try
                    {
                        // Attempt to save changes to the database
                        //await Task.Delay(500);

                        changeCount =  dbContext.SaveChanges();
                        saved = true;
                    }
                    catch (DbUpdateConcurrencyException ex)
                    {
                        foreach (var entry in ex.Entries)
                        {
                            if (entry.Entity is PhoneStock)
                            {
                                //最开始从数据库读取到的值
                                var predbValues = entry.OriginalValues;
                                //准备写入的值(在最开始读到的数据库基础值上作业务运算后得到的值)
                                var proposedValues = entry.CurrentValues;
                                //数据库当前值
                                var databaseValues = entry.GetDatabaseValues();
  
                                var amountNow = 0;
                                foreach (var property in proposedValues.Properties)
                                {
                                    if (property.Name == nameof(PhoneStock.Amount) )
                                    {
                                        //读到的数据库原始值
                                        var predbVal = (int)predbValues[property];
                                        //期望值
                                        var proposedValue = (int)proposedValues[property];
                                        //数据库当前值
                                        var databaseValue = (int)databaseValues[property];
                                        //处理解释:假设初始库存是10,A买1台,B买2台。因为并发关系,读取到的初始值predbVal都是10,假设A先执行库存设置,则这时databaseValue就是10-1=9,而B执行到这里时proposedValue就是10-2 = 8,我们要做的就算计算要减的值是初始值(10)-期望值(8)=2;再用当前数据库的实际值databaseValue-2即可得到最终的值
                                        //换成B先执行库存设置,则这时databaseValue就是10-2=8,这里时proposedValue就是10-1 = 9,初始值(10)-期望值(9)=1,再用当前数据库的实际值databaseValue-1=7得到同样的结果
                                        int diff = predbVal - proposedValue;
   
                                        if (proposedValue >= 0)
                                        {
                                            var currentVal = databaseValue - diff;
                                            if (currentVal >= 0)
                                            {
                                                //给期望值重新赋值后,再用savechange保存
                                                proposedValues[property] = currentVal;
                                            }
                                            else
                                            {
                                                //直接报库存不足,或者直接返回false
                                                //throw new Exception("库存不足");
                                                return false;
                                            }
  
                                        }
                                        else
                                        {
                                            //直接报库存不足,或者直接返回false
                                            //throw new Exception("库存不足");
                                            return false;
                                        }
                                        break;
  
                                    }
                                    // TODO: decide which value should be written to database
                                    // proposedValues[property] = <value to be saved>;
                                }

                                // Refresh original values to bypass next concurrency check
                                entry.OriginalValues.SetValues(databaseValues);
                            }
                            else
                            {
                                throw new NotSupportedException(
                                    "Don't know how to handle concurrency conflicts for "
                                    + entry.Metadata.Name);
                            }
                        }
                    }
                }
  
            }
            return changeCount > 0;

        }

标签:处理,数据库,并发,int,EFCore,var,public
From: https://www.cnblogs.com/summerZoo/p/18127105

相关文章

  • linux服务器 tcp高并发压测需要设置项
    linux服务器部署了一个tcp服务,需要进行并发压力测试 看看服务器能支持多少个tcp长连接  预计会有50w个连接需要设置linux不然是无法支持这么多连接的  如果达到这个值是无法建立新连接的 报错信息一般为 toomanyopenfiles 1, fs.file-max li......
  • Java高并发解决方案
    电商的秒杀和抢购,对我们来说,都不是一个陌生的东西。然而,从技术的角度来说,这对于Web系统是一个巨大的考验。当一个Web系统,在一秒钟内收到数以万计甚至更多请求时,系统的优化和稳定至关重要。这次我们会关注秒杀和抢购的技术实现和优化,同时,从技术层面揭开,为什么我们总是不容易抢到......
  • JAVA高并发的三种实现
    提到锁,大家肯定想到的是sychronized关键字。是用它可以解决一切并发问题,但是,对于系统吞吐量要求更高的话,我们这提供几个小技巧。帮助大家减小锁颗粒度,提高并发能力。初级技巧-乐观锁乐观锁使用的场景是,读不会冲突,写会冲突。同时读的频率远大于写。 悲观锁的实现: 悲观的......
  • 结合 tensorflow.js 、opencv.js 与 Ant Design 创建美观且高性能的人脸动捕组件并发
    系列文章目录如何在前端项目中使用opencv.js|opencv.js入门如何使用tensorflow.js实现面部特征点检测tensorflow.js如何从public路径加载人脸特征点检测模型tensorflow.js如何使用opencv.js通过面部特征点估算脸部姿态并绘制示意图tensorflow.js使用opencv.js将人......
  • Redis如何防止高并发?
    其实redis是不会存在并发问题的,因为他是单进程的,再多的命令都是一个接一个地执行的。我们使用的时候,可能会出现并发问题,比如获得和设定这一对。Redis的为什么有高并发问题?Redis的的出身决定Redis是一种单线程机制的nosql数据库,基于key-value,数据可持久化落盘。由于单线程所以red......
  • Java基础知识-面向对象编程(OOP)-Java集合框架-多线程和并发-Spring框架
    Java基础知识:Java的四种基本数据类型是:byte、short、int、long(整数类型)、float、double(浮点类型)、char(字符类型)、boolean(布尔类型)。它们之间的区别主要在于占用的内存大小和表示范围不同。Java中的String是不可变的意味着一旦String对象被创建,它的值就不能被修改。这意味着St......
  • sql server在高并发状态下同时执行Select查询与Update更新操作时的死锁问题
    最近在项目上线使用过程中使用SqlServer的时候发现在高并发情况下,频繁更新和频繁查询引发死锁。通常我们知道如果两个事务同时对一个表进行插入或修改数据,会发生在请求对表的X锁时,已经被对方持有了。由于得不到锁,后面的Commit无法执行,这样双方开始死锁。但是select语句和upda......
  • 掌握Go语言:Go 并发编程,轻松应对大规模任务处理和高并发请求(34)
    并发是Go语言的一个重要特性,通过goroutine和channel,Go提供了简单而强大的并发编程模型。goroutine是轻量级的线程,可以在程序中并发执行任务,而channel则是用于goroutine之间的通信的管道。Go并发应用场景1.并行计算应用场景:在需要处理大量计算密集型任务时,可......
  • 聊聊ChatGLM3多用户并发API调用的问题
    转载请备注出处:https://www.cnblogs.com/zhiyong-ITNote背景目前在公司内部4张A10的GPU服务器上部署了ChatGLM3开源模型;然后部署了官方默认的web_demo、api_demo两种模式;重新设计了前端,支持H5和安卓两个客户端调用。但却发现了不能并发访问的问题。问题现象在安卓与H5同时调......
  • 并发编程之Java中Selector
    系列文章目录文章目录系列文章目录前言前言前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。Selector提供选择执行已经就绪的任务的能力,使得多元I/O成为可能,就绪选......