我有一个SQL Server(2012),我使用Entity Framework(4.1)访问它。 在数据库中,我有一个名为URL的表,一个独立的进程在其中提供新的URL。 URL表中的条目可以处于“新建”,“处理中”或“已处理”状态。
我需要从不同的计算机访问网址表,检查状态为“新建”的网址条目,选择第一个并将其标记为“正在处理中”。
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序列成为原子以避免这种冲突?
答案 0 :(得分:49)
我只能通过手动向表发出锁定语句来实现此目的。这会产生完整表锁,所以要小心它!就我而言,创建一个我不希望一次触摸多个进程的队列非常有用。
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();
}
答案 1 :(得分:17)
我无法对安德烈的回答添加评论,但我对此评论感到担忧 " IsolationLevel.RepeatableRead将对所有读取的行应用锁定,使得如果表1已被线程1读取且线程1未完成事务,则线程2无法从表A读取。 #34;
可重复读只表示您将持有所有锁,直到交易结束。当您在事务中使用此隔离级别并读取一行(比如最大值)a"共享"锁定已发出,将一直持续到事务完成。这个共享锁将阻止另一个线程更新该行(更新将尝试对该行应用Exclusive锁并且将被现有共享锁阻止),但它将允许另一个线程读取该值(第二个线程)将在行上放置另一个共享锁 - 这是允许的(这就是为什么它们被称为共享锁))。因此,为了使上述语句正确,需要说" IsolationLevel.RepeatableRead将对所有读取的行应用锁定,使得线程2不能更新表A如果表1已被线程1读取且线程1未完成事务。"
对于原始问题,您需要使用可重复的读隔离级别并将锁升级为独占锁,以防止两个进程读取和更新相同的值。所有解决方案都涉及将EF映射到自定义SQL(因为升级锁定类型不是内置到EF中)。你可以使用jocull答案,或者你可以使用带有output子句的更新来锁定行(update语句总是获得Exclusive锁,在2008或以上可以返回结果集)。
答案 2 :(得分:11)
@jocull提供的答案很棒。我提供这个调整:
而不是:
"SELECT TOP 1 KeyColumn FROM MyTable WITH (TABLOCKX, HOLDLOCK)"
这样做:
"SELECT TOP 0 NULL FROM MyTable WITH (TABLOCKX)"
这更通用。您可以创建一个简单地将表名作为参数的辅助方法。无需知道数据(也就是任何列名),也无需在管道中实际检索记录(又名TOP 1
)
答案 3 :(得分:0)
您可以尝试将UPDLOCK提示传递给数据库并仅锁定特定的行。因此,它选择更新的内容还需要一个独占锁定,这样它可以保存更改(而不只是在开始时获取一个readlock)。 ,以便稍后在保存时稍后尝试升级)。上面的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