如何使用 Entity Framework 在读取时锁定表?

Posted

技术标签:

【中文标题】如何使用 Entity Framework 在读取时锁定表?【英文标题】:How can I lock a table on read, using Entity Framework? 【发布时间】:2012-11-04 10:31:59 【问题描述】:

我有一个使用实体框架 (4.1) 访问的 SQL Server (2012)。 在数据库中,我有一个名为 URL 的表,一个独立的进程向其中提供新的 URL。 URL 表中的条目可以处于“新建”、“处理中”或“已处理”状态。

我需要从不同的计算机访问 URL 表,检查状态为“New”的 URL 条目,取第一个并将其标记为“In Process”。

var newUrl = dbEntity.URLs.FirstOrDefault(url => url.StatusID == (int) URLStatus.New);
if(newUrl != null)

    newUrl.StatusID = (int) URLStatus.InProcess;
    dbEntity.SaveChanges();

//Process the URL

由于查询和更新不是原子的,我可以让两台不同的计算机读取和更新数据库中的同一个 URL 条目。

有没有办法让 select-then-update 序列原子化以避免这种冲突?

【问题讨论】:

【参考方案1】:

我只能通过手动向表发出锁定语句来真正实现这一点。这是一个完整的表锁,所以要小心!就我而言,它对于创建一个我不希望多个进程同时接触的队列很有用。

using (Entities entities = new Entities())
using (TransactionScope scope = new TransactionScope())

    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Complete();

更新 - 在 Entity Framework 6 中,尤其是使用 async / await 代码,您需要以不同的方式处理事务。在进行了一些转换后,这对我们来说是崩溃的。

using (Entities entities = new Entities())
using (DbContextTransaction scope = entities.Database.BeginTransaction())

    //Lock the table during this transaction
    entities.Database.ExecuteSqlCommand("SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)");

    //Do your work with the locked table here...

    //Complete the scope here to commit, otherwise it will rollback
    //The table lock will be released after we exit the TransactionScope block
    scope.Commit();

【讨论】:

这是一个非常好的解决方案。在我必须确保一次只有一个线程可以访问表的情况下,使用 IsolationLevel(我遇到了多重访问问题)比这里的其他解决方案效果更好。 即使使用此解决方案,当两个线程 A 和 B 同时执行时,它们都被锁定。这种情况有什么解决方案吗? @SQL.NETWarrior 你是说发生了死锁吗?这意味着是一个互斥锁,所以一个线程应该锁定另一个线程,但在完成后释放。我还没有遇到死锁,但是当你查询时要小心不要在其中打开另一个上下文。由于每次都没有以完全相同的方式锁定,因此我意外地造成了死锁。最好一次只对一张桌子进行操作。 当您唯一的方法是使用 TABLOCKX 完成这些 SELECT 时,此方法有效。它不会阻止没有提示的 SELECT,您可能会在应用程序的其他地方或 SQL Mng Studio 或其他地方使用这些提示。 您的异步问题可能与TransactionScope 默认选项有关,而SynchronizationContext 默认选项不适用。解决方案:new TransactionScope(scopeOption, transactionOption, TransactionScopeAsyncFlowOption.Enabled);【参考方案2】:

@jocull 提供的答案很棒。我提供了这个调整:

而不是这个:

"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"

这样做:

"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"

这是更通用的。您可以创建一个简单地将表名作为参数的辅助方法。无需知道数据(即任何列名),也无需实际检索管道中的记录(又名TOP 1

【讨论】:

你为什么省略holdlock?!没有它,语句完成后释放排他锁! 嗨,阿里。该锁在整个 EF 事务中保持活动状态。在我的工作中,我们经常在生产中使用它。您确实需要进行交易。 太棒了,我不知道你可以这样选择东西。我想知道是否有一种方法可以将表名参数化以安全地使其成为通用方法?【参考方案3】:

我无法对安德烈的回答添加评论,但我担心这条评论 “IsolationLevel.RepeatableRead 将对所有读取的行应用锁定,如果线程 1 已读取表 A 并且线程 1 未完成事务,则线程 2 无法从表 A 中读取。”

可重复只读表示您将持有所有锁直到事务结束。当您在事务中使用此隔离级别并读取一行(例如最大值)时,会发出“共享”锁并将一直保持到事务完成。此共享锁将阻止另一个线程更新该行(更新将尝试在该行上应用排他锁,这将被现有的共享锁阻塞),但它将允许另一个线程读取该值(第二个线程将在该行上放置另一个共享锁 - 这是允许的(这就是它们被称为共享锁的原因))。因此,为了使上述语句正确,它需要说“IsolationLevel.RepeatableRead 将对所有读取的行应用锁,这样线程 2 就无法更新表 A,如果表 A已被线程 1 读取,线程 1 未完成事务。”

对于最初的问题,您需要使用可重复的读取隔离级别并将锁升级为独占锁,以防止两个进程读取和更新相同的值。所有解决方案都涉及将 EF 映射到自定义 SQL(因为升级锁类型未内置在 EF 中)。您可以使用 jocull 答案,也可以使用带有输出子句的更新来锁定行(更新语句总是获得独占锁,并且在 2008 年或更高版本中可以返回结果集)。

【讨论】:

【参考方案4】:

您可以尝试将 UPDLOCK 提示传递给数据库并锁定特定行。这样它选择更新它的内容也会获得一个排他锁,以便它可以保存其更改(而不是一开始就获得一个读锁) ,它稍后会在保存时尝试升级)。上面jocull建议的Holdlock也是一个好主意。

private static TestEntity GetFirstEntity(Context context) 
return context.TestEntities
          .SqlQuery("SELECT TOP 1 Id, Value FROM TestEntities WITH (UPDLOCK)")
          .Single();

我强烈建议考虑乐观并发:https://www.entityframeworktutorial.net/EntityFramework5/handle-concurrency-in-entity-framework.aspx

【讨论】:

以上是关于如何使用 Entity Framework 在读取时锁定表?的主要内容,如果未能解决你的问题,请参考以下文章

Entity Framework Core 删除包含已读取实体的实体

从appsettings.json读取entity.Framework的ConnectionString

entity framework 使用存储过程删除数据后,还用再使用SaveChanges()保存吗?

如何将 Entity Framework Code First 与现有的 MS Access (.accdb) 数据库一起使用?

在没有 DbSet 的情况下查询 Entity Framework Core

Entity Framework Core:Guid 大于分页