Entity Framework——并发策略
使用EF框架遇到并发时,一般采取乐观并发控制。
1支持并发检验
为支持并发检验,需要对实体进行额外的设置。默认情况下是不支持并发检验的。有以下两种方式:
|
方式名称 |
说明 |
|
时间戳注解/行版本 |
使用TimestampAttribute特性,实体的属性必须是byte数组类型 |
|
非时间戳注解 |
使用ConcurrencyCheckAttribute |
|
Fluent API |
使用StringPropertyConfiguration.IsConcurrencyToken方法 |
注释
1)时间戳注解
- 一个类只能有一个属性可以配置为TimeStamp特性。
- 任何时候行内数据被修改时,数据库都会自动为此属性创建新值。
- 只要对相应的表执行更新操作,EF框架就会执行并发检测。
例:
[Timestamp]
public byte[] RowVersion { get; set; }
2)非时间戳注解
- 此方式,是对表的一个或多个字段进行并发检测
- 当更改一行时,EF框架就会执行并发检测。
例:
[ConcurrencyCheck]
public string Email { get; set; }
3)Fluent API
- 此方式,是对表的一个或多个字段进行并发检测
- 当更改一行时,EF框架就会执行并发检测。
例如:
public static void Set(DbModelBuilder modelBuilder)
{
//其他配置
modelBuilder.Entity<User>().Property(u => u.Email)
.IsRequired()
.IsUnicode(false)
.HasMaxLength()
.IsConcurrencyToken();
}
2乐观并发控制
2.1使用数据库中的数据(服务端胜)
使用DbEntityEntry.Reload方法加载数据库中的数据而不是使用当前实体的值。
例:
using (CustomDbContext context = new CustomDbContext())
{
var user = context.Users.Find();
user.Email = "eftxt8326@163.com";
bool saveFailed;
do
{
saveFailed = false; try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true;
ex.Entries.Single().Reload();
} } while (saveFailed);
}
分析:
当发生并发冲突时,context.SaveChanges();这行代码抛出异常DbUpdateConcurrencyException ,执行catch块的代码,ex.Entries.Single().Reload()这行代码作用是从数据库取出对应的一条记录然后用这条记录对当前实体赋值,又由于saveFailed = true,do语句块又执行一次,调用context.SaveChanges();将数据保存到数据库中,若这次执行do语句块,不抛出异常,由于 saveFailed = false,所以循环结束。
2.2使用当前实体数据(客户端胜)
使用当前实体数据覆盖数据库中的数据。
例:
using (CustomDbContext context = new CustomDbContext())
{
var user = context.Users.Find();
user.Email = "eftxt8326@163.com"; bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true; var entry = ex.Entries.Single();
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
} } while (saveFailed);
}
分析:
当发生并发冲突时,抛出DbUpdateConcurrencyException 异常,执行catch 块,ex.Entries.Single()这条语句的作用是从当前实体集中取出唯一的一个实体,然后调用DbEntityEntry.GetDatabaseValues,在数据库中查找这条记录,若能够找到这条记录,返回当前值的属性值集合。
entry.OriginalValues.SetValues这条语句的作用是:DbEntityEntry.OriginalValues指的是最后一次访问数据库时获得那条记录,调用DbPropertyValues.SetValues方法用一个词典给另一个词典赋值,entry.OriginalValues.SetValues(entry.GetDatabaseValues());是将当前数据库中的值赋给从数据库最后一次查出的值。由于saveFailed = true所以再次执行do语句块,将当前实体值写入数据库。
2.3结合当前实体值和数据库中的值
using (CustomDbContext context = new CustomDbContext())
{
var user = context.Users.Find();
user.Email = "eftxt8326@163.com"; bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true; var entry = ex.Entries.Single();
//获得当前实体值
var currentValues = entry.CurrentValues;
//获得数据库中的值
var databaseValues = entry.GetDatabaseValues(); //拷贝一份
var resolvedValues = databaseValues.Clone(); //对数据加工处理
HaveUserResolveConcurrency(currentValues, databaseValues, resolvedValues); entry.OriginalValues.SetValues(databaseValues);
entry.CurrentValues.SetValues(resolvedValues);
}
} while (saveFailed);
}
public void HaveUserResolveConcurrency(DbPropertyValues currentValues, DbPropertyValues databaseValues,
DbPropertyValues resolvedValues)
{
//对数据加工处理
}
也可以使用DbPropertyValues的public object ToObject()方法
using (CustomDbContext context = new CustomDbContext())
{
var user = context.Users.Find();
user.Email = "eftxt8326@163.com"; bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true; //获得当前实体值
var entry = ex.Entries.Single();
//获得数据库中的值
var databaseValues = entry.GetDatabaseValues();
var databaseValuesAsBlog = (User)databaseValues.ToObject(); //拷贝一份
var resolvedValuesAsBlog = (User)databaseValues.ToObject(); //对数据加工处理
HaveUserResolveConcurrency((User)entry.Entity,
databaseValuesAsBlog,
resolvedValuesAsBlog); entry.OriginalValues.SetValues(databaseValues);
entry.CurrentValues.SetValues(resolvedValuesAsBlog);
} } while (saveFailed);
} public void HaveUserResolveConcurrency(User entity,
User databaseValues,
User resolvedValues)
{
//对数据加工处理 }
3观察并发现象
本次实验选择观察“客户端胜”这种策略,选取这种策略的原因在不但可以通过试验观察到并发检测的情况,还可以观察到调用DbEntityEntry.GetDatabaseValues()、DbEntityEntry.OriginalValues、DbEntityEntry.CurrentValues的返回值,有助于深入理解这些概念
实体:使用ConcurrencyCheck特性标记实体属性
public class User
{
public int Id { get; set; }
/// <summary>
/// 账号
/// </summary>
public string Account { get; set; }
/// <summary>
/// 邮箱
/// </summary>
[ConcurrencyCheck]
public string Email { get; set; }
/// <summary>
/// 昵称
/// </summary>
public string Nickname { get; set; }
/// <summary>
/// 头像
/// </summary>
public string AvatarId { get; set; }
/// </summary>
/// 收藏
/// </summary>
public virtual ICollection<CollectionUser> CollectionUsers { get; set; }
/// <summary>
/// 记录插入时间
/// </summary>
public DateTime InsertTime { get; set; }
/// <summary>
/// 记录修改时间
/// </summary>
public DateTime UpdateTime { get; set; }
}
更新表users的Email字段:
为了可以观察到并发现象,采用多线程,测试发现,双核四线程处理器,两个并行任务,很难捕捉到并发现象;当并行任务数为三个以上时,可以很轻易地发现并发现象。同时我们会打印执行的SQL,来说明并发检测所依赖的基本原理。
public void ConALL()
{
var p = new ParallelOptions();
p.MaxDegreeOfParallelism = ;
Parallel.Invoke(p,() =>
{ ConM("1@163.com");
},
() =>
{
ConM("2@163.com");
},
() =>
{
ConM("3@163.com");
});
}
public void ConM(string s)
{
using (CustomDbContext context = new CustomDbContext())
{
var user = context.Users.Find(); user.Email = s; bool saveFailed;
do
{
saveFailed = false;
try
{
context.SaveChanges();
Trace.WriteLine(string.Format("【正常线程{1}】数据库中原值:{0}", user.Email, s));
Trace.WriteLine(string.Format("【正常线程{1}】客户端传值:{0}", s, s));
}
catch (DbUpdateConcurrencyException ex)
{
saveFailed = true; var entry = ex.Entries.Single();
var databaseValues = entry.GetDatabaseValues();
string em = databaseValues["Email"].ToString();
string or = entry.OriginalValues["Email"].ToString();
Trace.WriteLine(string.Format("【线程{1}】数据库中原值:{0}", user.Email, s));
Trace.WriteLine(string.Format("【线程{1}】客户端传值:{0}", s, s));
Trace.WriteLine(string.Format("【线程{1}】DbEntityEntry.GetDatabaseValues:{0}", em, s));
Trace.WriteLine(string.Format("【线程{1}】DbEntityEntry.OriginalValues:{0}", or, s));
entry.OriginalValues.SetValues(databaseValues);
} } while (saveFailed);
}
}
查看当前Mysql中的users表Email字段值为:1@163.com
执行程序,并记录结果:
执行的SQL
SELECT
`Extent1`.`Id`
`Extent1`.`Account`
`Extent1`.`Email`
`Extent1`.`Nickname`
`Extent1`.`AvatarId`
`Extent1`.`InsertTime`
`Extent1`.`UpdateTime`
FROM `Users` AS `Extent1`
WHERE `Extent1`.`Id` = @p0 LIMIT 2
-- p0: '1' (Type = Int32)
-- Executing at 2018/3/30 17:04:20 +08:00
-- Completed in 9 ms with result: EFMySqlDataReader
UPDATE `Users` SET `Email`=@gp1 WHERE (`Id` = 1) AND (`Email` = @gp2)
-- @gp1: '2@163.com' (Type = String IsNullable = false Size = 9)
-- @gp2: '1@163.com' (Type = String IsNullable = false Size = 9)
-- Executing at 2018/3/30 17:04:21 +08:00
-- Completed in 3 ms with result: 1
SELECT
`Extent1`.`Id`
`Extent1`.`Account`
`Extent1`.`Email`
`Extent1`.`Nickname`
`Extent1`.`AvatarId`
`Extent1`.`InsertTime`
`Extent1`.`UpdateTime`
FROM `Users` AS `Extent1`
WHERE `Extent1`.`Id` = @p0 LIMIT 2
SELECT
`Extent1`.`Id`
`Extent1`.`Account`
`Extent1`.`Email`
`Extent1`.`Nickname`
`Extent1`.`AvatarId`
`Extent1`.`InsertTime`
`Extent1`.`UpdateTime`
FROM `Users` AS `Extent1`
WHERE `Extent1`.`Id` = @p0 LIMIT 2
SELECT
`Extent1`.`Id`
`Extent1`.`Account`
`Extent1`.`Email`
`Extent1`.`Nickname`
`Extent1`.`AvatarId`
`Extent1`.`InsertTime`
`Extent1`.`UpdateTime`
FROM `Users` AS `Extent1`
WHERE `Extent1`.`Id` = @p0 LIMIT 2
-- p0: '1' (Type = Int32)
-- p0: '1' (Type = Int32)
-- p0: '1' (Type = Int32)
-- Executing at 2018/3/30 17:06:12 +08:00
-- Executing at 2018/3/30 17:06:12 +08:00
-- Executing at 2018/3/30 17:06:12 +08:00
-- Completed in 8 ms with result: EFMySqlDataReader
-- Completed in 8 ms with result: EFMySqlDataReader
-- Completed in 8 ms with result: EFMySqlDataReader
UPDATE `Users` SET `Email`=@gp1 WHERE (`Id` = 1) AND (`Email` = @gp2)
UPDATE `Users` SET `Email`=@gp1 WHERE (`Id` = 1) AND (`Email` = @gp2)
-- @gp1: '3@163.com' (Type = String IsNullable = false Size = 9)
-- @gp1: '1@163.com' (Type = String IsNullable = false Size = 9)
-- @gp2: '2@163.com' (Type = String IsNullable = false Size = 9)
-- Executing at 2018/3/30 17:06:12 +08:00
-- @gp2: '2@163.com' (Type = String IsNullable = false Size = 9)
-- Executing at 2018/3/30 17:06:12 +08:00
-- Completed in 3 ms with result: 1
-- Completed in 3 ms with result: 0
SELECT
`Limit1`.`Id`
`Limit1`.`Account`
`Limit1`.`Email`
`Limit1`.`Nickname`
`Limit1`.`AvatarId`
`Limit1`.`InsertTime`
`Limit1`.`UpdateTime`
FROM (SELECT
`Extent1`.`Id`
`Extent1`.`Account`
`Extent1`.`Email`
`Extent1`.`Nickname`
`Extent1`.`AvatarId`
`Extent1`.`InsertTime`
`Extent1`.`UpdateTime`
FROM `Users` AS `Extent1`
WHERE `Extent1`.`Id` = @p0 LIMIT 2) AS `Limit1`
-- p0: '1' (Type = Int32)
-- Executing at 2018/3/30 17:06:12 +08:00
-- Completed in 1 ms with result: EFMySqlDataReader
UPDATE `Users` SET `Email`=@gp1 WHERE (`Id` = 1) AND (`Email` = @gp2)
-- @gp1: '1@163.com' (Type = String IsNullable = false Size = 9)
-- @gp2: '3@163.com' (Type = String IsNullable = false Size = 9)
-- Executing at 2018/3/30 17:06:14 +08:00
-- Completed in 0 ms with result: 1
分析SQL
日志中出现Completed in 0 ms with result: 0,这说明某一次更新任务是失败的,这应该就出现并发更新的那一次,由于创建了三个并行的任务,所以从打印的日志中比较难以分辨是哪两次更新时发生并发,但是可以通过后面观察打印变量值来判断。这里的日志信息还展示了每条SQL执行的时。
观察上面的SQL语句,发现每个UPDATE 语句都有一个WHERE条件,尤为特别的是`Email` = @gp2,并发检测就是依赖这条语句实现的。当两个线程同时向数据库提交更新任务时,由于其中一个线程已将Email字段值更改,那么另一个线程执行的SQL由于不满足Email字段的匹配条件而修改失败,进而抛出OptimisticConcurrencyException异常。如果查看未配置并发检测生成的UPDATE 语句会更清楚这一点。
未配置并发检测生成的UPDATE 语句:
UPDATE `Users` SET `Email`=@gp1 WHERE `Id` = 1
各个变量的值
【正常线程2@163.com】数据库中原值:2@163.com
【正常线程2@163.com】客户端传值:2@163.com
“System.Data.Entity.Core.OptimisticConcurrencyException”类型的第一次机会异常在 EntityFramework.dll 中发生
“System.Data.Entity.Core.OptimisticConcurrencyException”类型的第一次机会异常在 EntityFramework.dll 中发生
“System.Data.Entity.Core.OptimisticConcurrencyException”类型的第一次机会异常在 EntityFramework.dll 中发生
“System.Data.Entity.Infrastructure.DbUpdateConcurrencyException”类型的第一次机会异常在 EntityFramework.dll 中发生
【正常线程3@163.com】数据库中原值:3@163.com
【正常线程3@163.com】客户端传值:3@163.com
【线程1@163.com】数据库中原值:1@163.com
【线程1@163.com】客户端传值:1@163.com
【线程1@163.com】DbEntityEntry.GetDatabaseValues:3@163.com
【线程1@163.com】DbEntityEntry.OriginalValues:2@163.com
【正常线程1@163.com】数据库中原值:1@163.com
【正常线程1@163.com】客户端传值:1@163.com
分析各个变量值
打印【正常线程】这行文本的代码在context.SaveChanges();这行代码之后,这说明如果能够打印出这行代码,那么就没有发生并发异常,所以上面在发生并发异常之前2@163.com和3@163.com这两个值都成功更新了Email字段,当要使用值1@163.com更新Email字段时,发生了并发异常。使用值2@163.com更新字段发生在使用3@163.com更新字段之前,所以发生并发异常时,数据库中的Email字段值为3@163.com,因此DbEntityEntry.GetDatabaseValues值为3@163.com,而DbEntityEntry.OriginalValues的值为2@163.com。
参考:
https://docs.microsoft.com/en-us/ef/
转载与引用请注明出处。 时间仓促,水平有限,如有不当之处,欢迎指正。
Entity Framework——并发策略的更多相关文章
- Entity Framework 6 开发系列 目录
2014 年开始接触 Entity Framework 6 也快两年,用它已经沉淀了一个成熟架构,也用来开发了不少大大小小的产品和项目,直到这段时间,才真正有时间来回顾,重新学习它,为让大家更加了解E ...
- Entity Framework 数据库初始化四种策略
策略一:数据库不存在时重新创建数据库 Database.SetInitializer<testContext>(new CreateDatabaseIfNotExists<testC ...
- Entity Framework数据库初始化四种策略
策略一:数据库不存在时重新创建数据库 程序代码 Database.SetInitializer<testContext>(new CreateDatabaseIfNotExists< ...
- EntityFramework_MVC4中EF5 新手入门教程之七 ---7.通过 Entity Framework 处理并发
在以前的两个教程你对关联数据进行了操作.本教程展示如何处理并发性.您将创建工作与各Department实体的 web 页和页,编辑和删除Department实体将处理并发错误.下面的插图显示索引和删除 ...
- Entity FrameWork初始化数据库的四种策略
程序猿就是苦逼,每天还得分出一些时间去写博文.天真的很热,今天就随便写一点啦! 1.EF初始化数据库的四中策略 EF可以根据项目中的模型自动创建数据库.下面我们就分类看看Entity Framewor ...
- entity framework如何控制并发
entity framework如何控制并发 针对字段http://msdn.microsoft.com/en-us/library/vstudio/bb738618(v=vs.100).aspx ...
- Entity Framework 处理并发
Entity Framework 处理并发 在以前的两个教程你对关联数据进行了操作.本教程展示如何处理并发性.您将创建工作与各Department实体的 web 页和页,编辑和删除Department ...
- Entity Framework Code First实现乐观并发
Entity Framework Code First实现乐观并发 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: h ...
- Entity Framework 数据并发访问错误原因分析与系统架构优化
博客地址 http://blog.csdn.net/foxdave 本文主要记录近两天针对项目发生的数据访问问题的分析研究过程与系统架构优化,我喜欢说通俗的白话,高手轻拍 1. 发现问题 系统新模块上 ...
随机推荐
- jQuery框架-1.基础知识
jQuery简介 jQuery,顾名思义是JavaScript和查询(Query),jQuery是免费.开源的.它可以简化查询DOM对象.处理事件.制作动画.处理Ajax交互过程且兼容多浏览器的jav ...
- 外网如何访问 Service?- 每天5分钟玩转 Docker 容器技术(139)
除了 Cluster 内部可以访问 Service,很多情况我们也希望应用的 Service 能够暴露给 Cluster 外部.Kubernetes 提供了多种类型的 Service,默认是 Clus ...
- js小括号的作用
js中小括号()的用法详解:对于小括号无论是菜鸟还是高手一定都不会陌生,可以说它几乎是随处可见,虽然熟悉但并非真正的理解,由此可能会产生很多莫名其妙的错误,下面就通过代码实例详细介绍一下小括号的用法. ...
- xml的SAX解析规则
一,为什么要用它 1.1,讲解 DOM解析原理:一次性把xml文档加载进内存,然后在内存中构建Document树. 对内存要求比较要. 缺点: 不适合读取大容量的xml文件,容易导致内存溢出. SAX ...
- Directory Opus(DO) 个人使用经验 1.0
设置语言为中文 即时过滤器 设置好之后,在文件目录直接先点击“ ; ”键,然后就可以即时过滤了. 自带的图片查看器查看图片时适应窗口 设置默认窗口 将当前打开的窗口配置为默认窗口,以后每次重新打开DO ...
- 圆方树简介(UOJ30:CF Round #278 Tourists)
我写这篇博客的原因 证明我也是学过圆方树的 顺便存存代码 前置技能 双联通分量:点双 然后就没辣 圆方树 建立 新建一个图 定义原图中的所有点为圆点 对于每个点双联通分量(只有两个点的也算) 建立一个 ...
- 记录一个前端bug的解决过程
人在江湖飘,哪能不挨刀. 我挨了重重一bug.严格来讲这可能是我职业生涯以来的首个悲惨经历,因为凭我的知识储备和经验,基本上任何可重现的bug都是可解的.然而这个bug却困扰了我三个月之久,它具有以下 ...
- php seaslog的使用
今天有幸在慕课网看到了 关于php日志处理工具 seasLog 的使用视频,本着好奇看完了该视频,觉得不错,便自己也倒腾了下,现在整理出来 seaslog github: https://githu ...
- intellij idea快捷键字典
最近在重装系统,在安装python IDE时候依然安装了sublime Text3和intellij Idea(冏,别问为什么没安装pycharm,0-0 逃).首先是已然将之前一直使用的sublim ...
- Unity中List的随机排序(乱序)
为什么要给List排序做一个Unity限定条件呢 首先,是C#中的List泛型,若是Java,直接调用Collection.shuffle()就OK了 而Unity的C#版本较低,不能使用Random ...