如何使用实体框架锁定读表?

我有一个 SQL Server (2012) ,我使用实体框架(4.1)访问它。 在数据库中,我有一个名为 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 序列成为原子序列以避免这种冲突?

69877 次浏览

我只能通过手动向表发出 lock 语句来实现这一点。这是一个 完整表锁,所以要小心!在我的例子中,它对于创建一个不希望同时触及多个进程的队列非常有用。

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();
}

更新 -在实体框架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();
}

我不能对安德烈的回答加以评论,但我对这一评论感到担忧 ”隔离级别。如果表 A 已被线程1读取,而线程1没有完成事务,那么可重复读取将对所有读取的行应用一个锁,使得线程2无法从表 A 中读取。”

可重复读只表示您将持有所有锁直到事务结束。当您在事务中使用这个隔离级别并读取一行(比如说最大值)时,会发出一个“ Shared”锁,并将持有该锁直到事务完成。这个共享锁将阻止另一个线程更新行(更新将尝试在行上应用一个独占锁,这个锁将被现有的共享锁阻塞) ,但它将允许另一个线程读取值(第二个线程将在行上放置另一个共享锁——这是允许的(这就是为什么它们被称为共享锁))。因此,为了使上面的语句正确,它需要说“隔离级别”。如果表 A 已被线程1读取,而且线程1没有完成事务,那么可重复读取将对所有行应用一个锁,以便线程2不能 更新表 A。”

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

@ jocull 给出的答案很棒,我提供以下这个调整:

而不是这样:

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

这样做:

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

这个更普通。您可以创建一个帮助器方法,它只是将表名作为参数。不需要知道数据(也就是任何列名) ,也不需要实际检索管道中的记录(也就是 TOP 1)

You could try passing a UPDLOCK hint to the database and just lock specific rows.. So that what it selects to update it also aquires an exclusive lock on so it can save its changes (rather than just acquiring a readlock in the beginning, that it later tries to upgrade later when saving). The Holdlock suggested by jocull above is also a good idea too.

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

想要分享我以上答案的组合:

public class Repository<TEntity, TKey>
: IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
protected readonly DbContext DbContext;


...


private sealed class Transaction<TTransactionEntity> : IDisposable
{
private readonly IDbContextTransaction dbTransaction;


public Transaction(DbContext context)
{
var tableName = context.Model
.FindEntityType(typeof(TTransactionEntity))
.GetTableName();


this.dbTransaction = context.Database
.BeginTransaction(IsolationLevel.RepeatableRead);


context.Database
.ExecuteSqlRaw($"SELECT TOP 0 NULL FROM {tableName} WITH (TABLOCKX)");
}


public void Dispose()
{
this.dbTransaction.Commit();
this.dbTransaction.Dispose();
}
}


public IDisposable LockingTransaction()
=> new Transaction<TEntity>(this.DbContext);
}

用途:

using (this.unitOfWork.MyRepository.LockingTransaction())
{
...
}