我有一个EF代码第一个上下文,它表示处理应用程序可以检索和运行的作业队列。这些处理应用程序可以在不同的计算机上运行,但指向同一个数据库。
上下文提供了一种方法,如果有任何工作要做,则返回QueueItem
,或者为null CollectQueueItem
。
为确保没有两个应用程序能够获得相同的作业,该集合将在ISOLATION LEVEL
REPEATABLE READ
的事务中进行。这意味着如果有两次尝试同时获取同一个作业,则会选择一个作为deadlock victim
并回滚。我们可以通过捕获DbUpdateException
并返回null
来处理此问题。
以下是CollectQueueItem
方法的代码:
public QueueItem CollectQueueItem()
{
using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead }))
{
try
{
var queueItem = this.QueueItems.FirstOrDefault(qi => !qi.IsLocked);
if (queueItem != null)
{
queueItem.DateCollected = DateTime.UtcNow;
queueItem.IsLocked = true;
this.SaveChanges();
transaction.Complete();
return queueItem;
}
}
catch (DbUpdateException) //we might have been the deadlock victim. No matter.
{ }
return null;
}
}
我在LinqPad中进行了测试,检查这是否按预期工作。以下是测试:
var ids = Enumerable.Range(0, 8).AsParallel().SelectMany(i =>
Enumerable.Range(0, 100).Select(j => {
using (var context = new QueueContext())
{
var queueItem = context.CollectQueueItem();
return queueItem == null ? -1 : queueItem.OperationId;
}
})
);
var sw = Stopwatch.StartNew();
var results = ids.GroupBy(i => i).ToDictionary(g => g.Key, g => g.Count());
sw.Stop();
Console.WriteLine("Elapsed time: {0}", sw.Elapsed);
Console.WriteLine("Deadlocked: {0}", results.Where(r => r.Key == -1).Select(r => r.Value).SingleOrDefault());
Console.WriteLine("Duplicates: {0}", results.Count(r => r.Key > -1 && r.Value > 1));
//IsolationLevel = IsolationLevel.RepeatableRead:
//Elapsed time: 00:00:26.9198440
//Deadlocked: 634
//Duplicates: 0
//IsolationLevel = IsolationLevel.ReadUncommitted:
//Elapsed time: 00:00:00.8457558
//Deadlocked: 0
//Duplicates: 234
我跑了几次测试。如果没有REPEATABLE READ
隔离级别,则不同的theads会检索相同的作业(在234个重复项中看到)。使用REPEATABLE READ
,作业只会被检索一次,但性能会受到影响,并且会有634个死锁事务。
我的问题是:有没有办法在EF中获得此行为而没有死锁或冲突的风险?我知道在现实生活中会有较少的争用,因为处理器不会不断地访问数据库,但是,有没有办法安全地执行此操作而无需处理DbUpdateException?我可以将性能提升到没有REPEATABLE READ
隔离级别的版本吗?或者死锁实际上并没有那么糟糕,我可以安全地忽略异常并让处理器在几毫秒后重试并接受如果并非所有交易同时发生,性能将会正常?
提前致谢!
答案 0 :(得分:1)
我推荐一种不同的方法。
a)sp_getapplock Use an SQL SP that provides an Application lock feature 因此,您可以拥有独特的应用行为,这可能涉及从数据库中读取或您需要控制的其他任何活动。它还允许您以正常方式使用EF。
OR
b)乐观并发 http://msdn.microsoft.com/en-us/data/jj592904
//Object Property:
public byte[] RowVersion { get; set; }
//Object Configuration:
Property(p => p.RowVersion).IsRowVersion().IsConcurrencyToken();
APP锁的逻辑扩展或单独使用的是DB上的rowversion并发字段。允许脏读。但是当有人去更新记录时,如果有人殴打它,它就会失败。开箱即用EF乐观锁定。 您可以稍后删除“收集”的工作记录。
除非您期望高水平的并发性,否则这可能是更好的方法。
答案 1 :(得分:1)
根据Phil的建议,我使用乐观并发来确保不能多次处理作业。我意识到,不必添加专用的rowversion
列,我可以使用IsLocked位列作为ConcurrencyToken
。从语义上讲,如果自从我们检索行以来此值已更改,则更新应该失败,因为只有一个处理器应该能够锁定它。我使用下面的fluent API进行配置,虽然我也可以使用ConcurrencyCheck data annotation。
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<QueueItem>()
.Property(p => p.IsLocked)
.IsConcurrencyToken();
}
然后我能够简化CollectQueueItem
方法,完全丢失TransactionScope并捕获更多DbUpdateConcurrencyException
。
public OperationQueueItem CollectQueueItem()
{
try
{
var queueItem = this.QueueItems.FirstOrDefault(qi => !qi.IsLocked);
if (queueItem != null)
{
queueItem.DateCollected = DateTime.UtcNow;
queueItem.IsLocked = true;
this.SaveChanges();
return queueItem;
}
}
catch (DbUpdateConcurrencyException) //someone else grabbed the job.
{ }
return null;
}
我重新考试,你可以看到它是一个很好的妥协。没有重复,比REPEATABLE READ
快近100倍,没有DEADLOCKS
所以DBA不会出现在我的案例中。真棒!
//Optimistic Concurrency:
//Elapsed time: 00:00:00.5065586
//Deadlocked: 624
//Duplicates: 0