1

我有一个 EF 代码优先上下文,它表示处理应用程序可以检索和运行的作业队列。这些处理应用程序可以在不同的机器上运行,但指向同一个数据库。

上下文提供了一个方法,QueueItem如果有任何工作要做,则返回 a 或 null,称为CollectQueueItem

为了确保没有两个应用程序可以获取相同的作业,收集发生在一个带有ISOLATION LEVELof的事务中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隔离级别的情况下获得更接近版本的性能吗?或者死锁实际上并没有那么糟糕,我可以放心地忽略异常,让处理器在几毫秒后重试,并接受如果不是所有事务都同时发生,性能会很好?

提前致谢!

4

2 回答 2

1

我推荐一种不同的方法。

a) sp_getapplock 使用提供应用程序锁定功能的 SQL SP 因此您可以拥有独特的应用程序行为,这可能涉及从数据库读取或您需要控制的其他活动。它还允许您以正常方式使用 EF。

或者

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 乐观锁定。您可以稍后轻松删除“收集”的作业记录。

除非您期望高水平的并发性,否则这可能是更好的方法。

于 2013-07-31T13:42:11.307 回答
1

正如 Phil 所建议的,我使用乐观并发来确保作业不能被多次处理。我意识到,不必添加专用rowversion列,我可以使用 IsLocked 位列作为ConcurrencyToken. 从语义上讲,如果这个值在我们检索到该行后发生了变化,那么更新应该会失败,因为只有一个处理器应该能够锁定它。我使用如下fluent API来配置它,虽然我也可以使用ConcurrencyCheck 数据注释

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

我重新进行了测试,你可以看到这是一个很好的妥协。没有重复,比 with 快近 100 倍REPEATABLE READDEADLOCKS所以 DBA 不会在我的案子上。惊人的!

//Optimistic Concurrency:
//Elapsed time: 00:00:00.5065586
//Deadlocked: 624
//Duplicates: 0
于 2013-07-31T16:22:00.967 回答