EFCore 高并发

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

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

当前Demo:EFConcurrentDemo

Github:summerZoo123/EFConcurrentDemo (github.com)

数据库:

CREATE TABLE `phone` (
`Id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`Name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`Price` decimal(10,2) NOT NULL,
`PublishTime` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
`Amount` int NOT NULL,
`RowVer` char(36) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '版本号',
PRIMARY KEY (`Id`)
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb3; -- ----------------------------
-- Records of phone
-- ----------------------------
INSERT INTO `phone` VALUES ('31', '华为Mate60', '6000.00', '2024-04-08 15:00:26', '10', '4abff008-122f-4f0d-9e3b-10863e7a2e3c');
INSERT INTO `phone` VALUES ('32', '小米14', '5000.00', '2024-04-10 10:59:41', '100', '0483330a-ab45-4d66-9e44-e60e072febe7');
INSERT INTO `phone` VALUES ('33', '一加12', '4000.00', '2024-04-08 15:00:26', '30', '1b004b2f-744e-4e55-bc7b-238c0e766e83');

什么是高并发

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

常见的高并发场景

  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();
}

行版本属性-(数据库生成,仅限sqlserver)

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; }

EFCore 高并发的更多相关文章

  1. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  2. 如何在高并发分布式系统中生成全局唯一Id

    月整理出来,有兴趣的园友可以关注下我的博客. 分享原由,最近公司用到,并且在找最合适的方案,希望大家多参与讨论和提出新方案.我和我的小伙伴们也讨论了这个主题,我受益匪浅啊…… 博文示例: 1.     ...

  3. 协程--gevent模块(单线程高并发)

    先恶补一下知识点,上节回顾 上下文切换:当CPU从执行一个线程切换到执行另外一个线程的时候,它需要先存储当前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指针等,最后才开始执行.这种 ...

  4. mysql高并发和表类型

    高并发:http://www.cnblogs.com/wangchaozhi/p/5061378.html 表类型:http://www.xiaoxiaozi.com/2009/07/14/1171/

  5. 分布式大数据高并发的web开发框架

    一.引言 通常我们认为静态网页html的网站速度是最快的,但是自从有了动态网页之后,很多交互数据都从数据库查询而来,数据也是经常变化的,除了一些新闻资讯类的网站,使用html静态化来提高访问速度是不太 ...

  6. PHP uniqid 高并发生成不重复唯一ID

    http://www.51-n.com/t-4264-1-1.html PHP uniqid()函数可用于生成不重复的唯一标识符,该函数基于微秒级当前时间戳.在高并发或者间隔时长极短(如循环代码)的情 ...

  7. 【实战Java高并发程序设计 7】让线程之间互相帮助--SynchronousQueue的实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  8. 【实战Java高并发程序设计6】挑战无锁算法:无锁的Vector实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  9. 【实战Java高并发程序设计 5】让普通变量也享受原子操作

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  10. 【实战Java高并发程序设计 4】数组也能无锁:AtomicIntegerArray

    除了提供基本数据类型外,JDK还为我们准备了数组等复合结构.当前可用的原子数组有:AtomicIntegerArray.AtomicLongArray和AtomicReferenceArray,分别表 ...

随机推荐

  1. ceph数据重构原理

    本文分享自天翼云开发者社区<ceph数据重构原理>,作者:x****n 在分布式存储系统Ceph中,硬盘故障是一种常见问题.为了保证数据安全,当发生硬盘故障后,分布式存储系统会依据算法对故 ...

  2. JMeter的CLI模式(非GUI模式)常用命令

    JMeter的CLI模式(非GUI模式)常用命令 Apache JMeter是一款强大的开源性能测试工具,它支持图形用户界面(GUI)模式和非图形用户界面(CLI,即Command Line Inte ...

  3. IDEA中创建Spring Boot项目(SSM框架)

    一.IDEA创建新Maven项目 创建maven项目完成 因为创建多模块项目,删除根目录src目录 二.maven多模块项目配置 需要创建的模块 umetric-web  控制层 umetric-we ...

  4. 【忍者算法】从图书馆找书到矩阵搜索:探索二维矩阵中的高效搜索|LeetCode第240题 搜索二维矩阵 II

    从图书馆找书到矩阵搜索:探索二维矩阵中的高效搜索 生活中的搜索策略 想象你在一个大型图书馆里找书.这个图书馆的书架是按照两个维度排列的:每个书架从左到右按书名字母顺序排列,从上到下的书架则按照出版年份 ...

  5. 面试官最想听到的Vue和React区别

    前言 欧阳最近找工作面试时总是被问到两个问题:Vue和React的区别和从编译原理的角度来聊聊Vue的template和React的jsx.面试官问这些问题一般是想了解你对这两个框架的理解,所以这是一 ...

  6. BUUCTF-Web方向16-20wp

    [极客大挑战 2019]PHP 由内容提示应该存在源码备份,常见的如下,一个个尝试 后缀:tar tar.gz zip rar 名字:www web website backup back wwwro ...

  7. Studio 3T 试用期破解(含破解补丁) - 解决办法

    使用数据可视化工具Studio 3T查看MongoDB数据集数据,但是Studio 3T试用过期了,没了权限打不开软件怎么办? 这里通过建立批处理文件,重置试用时间,即可临时破解权限. 每次开机重启脚 ...

  8. java 8 lamdba 表达式list集合的BigDecimal求和操作

  9. Kettle - 核心概念

    可视化编程 转换 步骤(Step) 跳(Hop) 元数据 数据类型 并行 作业 可视化编程 kettle 可以被归类为可视化编程语言(Visula Programming Languages,VPL) ...

  10. Zookeeper - 客户端常用命令

    查看客户端命令帮助信息 查看Zookeeper的版本 查看使用过的历史命令 查看根目录下的znode 创建znode 查看节点信息 修改znode的内容 删除znode 关闭连接 连接客户端 退出客户 ...