Entity Framework 并发冲突解决方案(上)

简介: Entity Framework 并发冲突解决方案

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


1.乐观并发

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


2.悲观并发

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


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


零、方法一

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

image.png

下面我们来修改一下 Name 字段的值:

class 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 列被更新为了最后一次提交值王五,如下图所示:

image.png

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

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


1.并发 Token

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

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

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

public byte[] RowVersion { get; set; }

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

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

这样在每次提交修改请求时 Entity Framework 都会检查数据库中的行版本和当前提交数据的行版本是否一致,如果一直就更新数据和行版本信息。


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

class 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 的原始值和更新后的数据库值以及当前值从哪里获得。代码如下:

try
{
  //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 方法来更新内存中的最新值,而是直接利数据库值更新当前内存中的值,如果你想到这里说明你已经掌握了解决并发冲突最简单的方法。那么我们就来看一下代码:

try
{
    //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 异常,将不在进行处理。我们来看以下代码:

class 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. 数据库获胜
如果你想让数据库获胜,那就简单了。再发生异常时不需做任何处理,只返回方法的返回值即可。我们将上一个例子的代码更新一下:

class 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月前
|
SQL 数据处理 数据库
提升数据处理效率:深入探讨Entity Framework Core中的批量插入与更新操作及其优缺点
【8月更文挑战第31天】在软件开发中,批量插入和更新数据是常见需求。Entity Framework Core 提供了批处理功能,如 `AddRange` 和原生 SQL 更新,以提高效率。本文通过对比这两种方法,详细探讨它们的优缺点及适用场景。
112 0
|
3月前
|
API 数据库 开发者
掌握数据完整性的关键:全面解析Entity Framework Core中的事务管理策略及其应用
【8月更文挑战第31天】在数据库操作中,确保数据完整性至关重要。Entity Framework Core(EF Core)作为一款强大的ORM工具,提供了丰富的API支持事务管理,帮助开发者实现数据的一致性和完整性。
45 0
|
3月前
|
存储 测试技术 数据库
Entity Framework Core Migrations 超厉害!轻松实现数据库版本控制,让你的开发更顺畅!
【8月更文挑战第31天】数据库的演变是软件开发中不可或缺的部分。随着应用发展,数据库需不断调整以适应新功能。Entity Framework Core Migrations 作为数据库的守护者,提供强大的版本控制手段,确保不同环境下的数据库一致性。通过创建和管理迁移脚本,开发者可以有序地管理数据库变更,避免混乱和数据丢失。安装并配置好 EF Core 后,可以通过命令行工具轻松创建、应用及回滚迁移,如 `dotnet ef migrations add InitialMigration` 和 `dotnet ef database update`。
40 0
|
3月前
|
存储 缓存 数据库连接
Entity Framework Core 跨数据库查询超厉害!多数据库连接最佳实践,让你的开发更高效!
【8月更文挑战第31天】在现代软件开发中,跨数据库查询是常见需求。Entity Framework Core(EF Core)作为强大的ORM框架,支持多种方法实现这一功能。本文介绍了在EF Core中进行跨数据库查询的最佳实践,包括:理解数据库上下文、使用多个上下文进行查询、处理数据库连接与事务,以及性能优化策略。通过创建独立的数据库上下文如`UserContext`和`OrderContext`,并在业务逻辑中同时使用它们,可以轻松实现跨库查询。此外,利用`TransactionScope`可确保事务一致性,从而提高系统的可靠性和效率。
161 0
|
6月前
|
SQL 算法
基于若依的ruoyi-nbcio流程管理系统修改代码生成的sql菜单id修改成递增id(谨慎修改,大并发分布式有弊端)
基于若依的ruoyi-nbcio流程管理系统修改代码生成的sql菜单id修改成递增id(谨慎修改,大并发分布式有弊端)
99 1
|
数据库连接 数据库 C++
entity framework core在独立类库下执行迁移操作
entity framework core在独立类库下执行迁移操作
109 0
|
SQL 存储 前端开发
使用DbContextPool提高EfCore查询性能
① 提示EFCore2.0新推出的DbContextPool特性,有效提高SQL查询吞吐量 ② 尝试使用SQL Server 内置脚本自证会话中有效连接数
使用DbContextPool提高EfCore查询性能
|
运维 安全 数据库
Entity Framework 并发冲突解决方案(下)
Entity Framework 并发冲突解决方案
170 0
|
存储 SQL 数据库
Entity Framework Core 捕获数据库变动
Entity Framework Core 捕获数据库变动
182 0