范围内的DbContext的并发

时间:2018-10-02 17:42:16

标签: c# entity-framework asynchronous asp.net-core concurrency

我有一个带有标准DI DbContext的标准ASP.NET Core 2.0 Web API,

services.AdDbContext<TestContext>( ... );

我知道我的上下文将添加作用域生存期,这基本上意味着,每当我的API收到新请求时,它都会创建该上下文的新实例。

到目前为止,一切都很好。

现在,我创建了一个名为 TestRepository 的存储库,其中包含以下内容:

public class TestRepository : ITestRepository {
    public TestContext _context;
    public TestRepository(TestContext context) {
        _context = context;
    }

    public async Task IncrementCounter() {
        var row = await _context.Banks.FirstOrDefaultAsync();
        row.Counter += 1;
        await _context.SaveChangesAsync();
    }
}

因此,它基本上只是以1-异步的方式递增一列中的值。 ITestRepository已添加到服务IoC中,并在需要时注入。

场景:

用户A 调用API,获取TestContext的新实例,然后调用IncrementCounter方法。

之前,将SaveChangesAsync称为用户B ,已对该API进行了调用,获得了新的TestContext,并且还调用了IncrementCounter方法。

现在用户A 将值 1 保存到该列中,而用户B 也认为它应该保存值 1 ,但实际上应该为 2

即使在阅读了相关文档之后,我仍然不确定如何保证 IncrementCounter 方法是否正确递增,即使多个用户使用自己的 TestContext < / strong>。

我可能比应该给自己带来更多的困惑,但是有人可以澄清一下如何处理同一上下文的不同实例之间的并发吗?

相关文档:

更新1

我曾考虑过在新的'rowVersion'属性上实现[TimeStamp]数据注释,然后捕获DbConcurrencyException,如相关文档所述,但这确实解决了问题,因为它是一个有范围的DbContext,因此具有不同的上下文正在尝试SaveChangesAsync()吗?

1 个答案:

答案 0 :(得分:4)

实际上,这与对象生存期(无论是否为作用域)无关。并发就是并发。进行诸如增加计数器之类的操作本质上不是线程安全的,因此您必须使用锁或乐观并发,就像处理任何其他线程不安全的任务一样。通常不赞成使用数据库锁,因此,乐观并发无疑是首选方法。

这在数据库级别的工作原理是,您实际上具有一个timestamp列,该列随每次更改而更新。 EF足够聪明,可以监视此列,如果它在更新时具有的值与您第一次查询该行时的值不同,则它会中止更新。此时,您的代码将捕获异常并通过重新查询该行并尝试再次对其进行更新来对其进行处理。

例如,用户A和用户B尝试同时保存值1。假设用户B提前了几毫秒并成功更新了该列。然后,这还将更新timestamp列。用户A的更新到达时,更新失败,因为时间戳列不再匹配。这冒泡回到EF,并在其中抛出DbUpdateConcurrencyException。您的代码捕获到此异常,并通过重新查询该列(现在为1)而不是0(并获得最新时间戳)的行作为响应,递增到2,然后尝试再次保存。这次,没有其他用户在做任何事情,因此一切顺利。但是,您也可能有另一个并发异常。在这种情况下,您需要冲洗并重复,最终尝试将3保存到色谱柱中,依此类推。

您已经链接到问题本身的所有相关文档,因此,我建议您花一些时间来仔细阅读所有内容并真正理解它。您需要向存储增量值的实体添加新属性:

[Timestamp]
public byte[] Version { get; set; }

这样(显然在迁移之后),EF就会在发生冲突时如上所述在保存时引发异常。为了捕获和处理这些,我建议使用Polly之类的库。它使您可以设置重试策略,从而轻松处理这种重复的查询增量保存逻辑。