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

2乐观并发控制

2.1使用数据库中的数据(服务端胜)

使用DbEntityEntry.Reload方法加载数据库中的数据而不是使用当前实体的值。

例:

            using (CustomDbContext context = new CustomDbContext())
            {
                var user = context.Users.Find(1);
                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(1);
                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(1);
                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(1);
                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 = 4;
            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(1);
                
                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/ 

-----------------------------------------------------------------------------------------

转载与引用请注明出处。

时间仓促,水平有限,如有不当之处,欢迎指正。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏GreenLeaves

存储过程详解

存储过程简介 什么是存储过程:存储过程可以说是一个记录集吧,它是由一些T-SQL语句组成的代码块,这些T-SQL语句代码像一个方法一样实现一些功能(对单表或多表...

20810
来自专栏逸鹏说道

C# 温故而知新:Stream篇(四)下

上面的例子是将一个文件作为整体进行操作,这样会带来一个问题,当文件很大或者网络不是很稳定的时候会发生意想不到的错误 那我们该怎么解决...

3335
来自专栏MasiMaro 的技术博文

OLEDB存取BLOB型数据

现代数据库系统除了支持一些标准的通用数据类型以外,大多数还支持一种称之为BLOB型的数据。 BLOB全称为big large object bytes, 大二...

1223
来自专栏JavaEE

mybatis-plus的使用 ------ 入门

mybatis在持久层框架中还是比较火的,一般项目都是基于ssm。虽然mybatis可以直接在xml中通过SQL语句操作数据库,很是灵活。但正其操作都要通过SQ...

2.2K4
来自专栏静默虚空的博客

[设计模式]单例模式

简介 单例模式(Singleton Pattern)保证一个类只有一个实例,并提供一个访问它的全局访问点。 单例模式是一种对象创建型模式 (可参考 设计模式 ...

1989
来自专栏PingCAP的专栏

TiDB 源码阅读系列文章(十)Chunk 和执行框架简介

Chunk 本质上是 Column 的集合,它负责连续的在内存中存储同一列的数据,接下来我们看看 Column 的实现。

5.7K145
来自专栏编程

防止黑客SQL注入的方法

一、SQL注入简介 SQL注入是比较常见的网络攻击方式之一,它不是利用操作系统的BUG来实现攻击,而是针对程序员编程时的疏忽,通过SQL语句,实现无帐号登录,甚...

2217
来自专栏Java帮帮-微信公众号-技术文章全总结

高级教程-springData-JPA第一天【悟空教程】

ORM(Object-Relational Mapping) 表示对象关系映射。在面向对象的软件开发中,通过 ORM,就可以把对象映射到关系型数据库中。只要有一...

2903
来自专栏安恒网络空间安全讲武堂

听说thinkphp又出事了?

0x01 前言 听说thinkphp又出事了,之前看过一次tp5的源码,不过只看了查询(select)的过程,这次问题出在update和insert中,但是归根...

3174
来自专栏跟着阿笨一起玩NET

C#设计模式学习笔记-单例模式

  最近在学设计模式,学到创建型模式的时候,碰到单例模式(或叫单件模式),现在整理一下笔记。

792

扫码关注云+社区