69

我有一个使用 Entity Framework (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 序列原子化以避免这种冲突?

4

4 回答 4

60

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

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();
}
于 2014-03-28T19:43:17.087 回答
20

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

而不是这个:

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

做这个:

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

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

于 2016-07-18T15:59:47.997 回答
19

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

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

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

于 2015-01-08T18:20:01.973 回答
1

您可以尝试将 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

于 2019-07-22T11:40:49.527 回答