使用 Entity Framework 进行原子读写

Atomic Read and Write with Entity Framework

我有两个不同的进程(在不同的机器上)正在读取和更新数据库记录。

我需要确保的规则是,只有当记录的值为 "Initial" 时,才必须更新记录。另外,在提交之后我想知道它是否真的从当前进程更新了(如果值不是初始值)

现在,下面的代码执行如下操作:

var record = context.Records
             .Where(r => (r.id == id && r.State == "Initial"))
             .FirstOrDefault();

if(record != null) {
  record.State = "Second";
  context.SaveChanges();
}

现在有几个问题

1) 从代码中可以看出,在获取状态为 "Initial" 的记录后,一些其他进程可能已将其更新为状态 "Second",在此进程执行 SaveChanges 之前。 在这种情况下,我们不必要地将状态覆盖为相同的值。这是这里发生的情况吗?

2) 如果情况 1 不是发生的情况,那么 EntityFramework 可能会将上述内容翻译成类似

的内容
update Record set State = "Second" where Id = someid and State = "Initial"

并将其作为交易执行。这样只有一个进程写入值。 EF 默认 TransactionScope 是否属于这种情况?

在这两种情况下,我如何确定更新是从我的进程而不是其他进程进行的?

如果这是内存中的对象,那么在代码中它会转化为类似于假设多个线程访问相同数据结构的东西

Record rec = FindRecordById(id);
lock (someobject)
{
    if(rec.State == "Initial")
       {
          rec.State = "Second";
          //Now, that I know I updated it I can do some processing
       }
}

谢谢

一般来说,可以使用 2 种主要的并发模式:

  • 悲观并发:您锁定一行以防止其他人意外更改您当前尝试更新的数据。 EF 为此类并发模式提供任何本机支持。
  • 乐观并发:引自EF's documentation"Optimistic concurrency involves optimistically attempting to save your entity to the database in the hope that the data there has not changed since the entity was loaded. If it turns out that the data has changed then an exception is thrown and you must resolve the conflict before attempting to save again." 该模式受 EF 支持,可以使用起来相当简单。

关注 EF 确实支持的乐观并发选项,让我们比较您的示例在使用和不使用 EF 的乐观并发控制处理的情况下的行为方式。我假设您使用的是 SQL 服务器。

无并发控制

让我们从数据库中的以下脚本开始:

create table Record (
  Id int identity not null primary key,
  State varchar(50) not null
)

insert into Record (State) values ('Initial')

这里是带有 DbContextRecord 实体的代码:

public class MyDbContext : DbContext
{
    static MyDbContext()
    {
        Database.SetInitializer<MyDbContext>(null);
    }

    public MyDbContext() : base(@"Server=localhost;Database=eftest;Trusted_Connection=True;") { }

    public DbSet<Record> Records { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        modelBuilder.Configurations.Add(new Record.Configuration());
    }
}

public class Record
{
    public int Id { get; set; }

    public string State { get; set; }

    public class Configuration : EntityTypeConfiguration<Record>
    {
        public Configuration()
        {
            this.HasKey(t => t.Id);

            this.Property(t => t.State)
                .HasMaxLength(50)
                .IsRequired();
        }
    }
}

现在,让我们使用以下代码测试您的并发更新方案:

static void Main(string[] args)
{
    using (var context = new MyDbContext())
    {
        var record = context.Records
            .Where(r => r.Id == 1 && r.State == "Initial")
            .Single();

        // Insert sneaky update from a different context.
        using (var sneakyContext = new MyDbContext())
        {
            var sneakyRecord = sneakyContext.Records
                .Where(r => r.Id == 1 && r.State == "Initial")
                .Single();

            sneakyRecord.State = "Sneaky Update";
            sneakyContext.SaveChanges();
        }

        // attempt to update row that has just been updated and committed by the sneaky context.
        record.State = "Second";
        context.SaveChanges();
    }
}

如果跟踪 SQL,您会看到 update 语句如下所示:

UPDATE [dbo].[Record]
SET [State] = 'Second'
WHERE ([Id] = 1)

因此,实际上,它并不关心另一个事务偷偷进入更新。它只是盲目地覆盖其他更新所做的任何事情。因此,数据库中该行的 State 的最终值为 'Second'.

乐观并发控制

让我们调整我们的初始 SQL 脚本,将并发控制列包含到我们的 table:

create table Record (
  Id int identity not null primary key,
  State varchar(50) not null,
  Concurrency timestamp not null -- add this row versioning column
)

insert into Record (State) values ('Initial')

我们还要调整 Record 实体 class(DbContext class 保持不变):

public class Record
{
    public int Id { get; set; }

    public string State { get; set; }

    // Add this property.
    public byte[] Concurrency { get; set; }

    public class Configuration : EntityTypeConfiguration<Record>
    {
        public Configuration()
        {
            this.HasKey(t => t.Id);

            this.Property(t => t.State)
                .HasMaxLength(50)
                .IsRequired();

            // Add this config to tell EF that this
            // property/column should be used for 
            // concurrency checking.
            this.Property(t => t.Concurrency)
                .IsRowVersion();
        }
    }
}

现在,如果我们尝试重新 运行 与之前场景相同的 Main() 方法,您会注意到 update 语句的生成方式和执行:

UPDATE [dbo].[Record]
SET [State] = 'Second'
WHERE (([Id] = 1) AND ([Concurrency] = <byte[]>))
SELECT [Concurrency]
FROM [dbo].[Record]
WHERE @@ROWCOUNT > 0 AND [Id] = 1

请特别注意 EF 如何在 update 语句的 where 子句中自动包含为并发控制定义的列。

在这种情况下,因为实际上存在并发更新,EF 会检测到它,并在该行抛出 DbUpdateConcurrencyException 异常:

context.SaveChanges();

因此,在这种情况下,如果您检查数据库,您会看到相关行的 State 值为 'Sneaky Update',因为我们的第二次更新未能通过并发检查。

最后的想法

如您所见,在 EF 中激活自动乐观并发控制不需要做太多工作。

不过,棘手的地方在于,抛出 DbUpdateConcurrencyException 异常时如何处理?在这种情况下,主要由您决定要做什么。但有关该主题的进一步指导,您可以在此处找到更多信息:EF - Optimistic Concurrency Patterns.