避免重复使用同一数据库表的并发读取

时间:2012-07-28 12:49:11

标签: c# sql-server entity-framework concurrency

避免重复使用同一数据库表的并发读取

我们有一个包含任务列表的表

Table RecordsTable  
    RecordID
    RecordName
    ...
    ...
    IsProcessed

多个工作者机器读取表格,一旦处理完任务,就将IsProcessed标记为真。

因此,如果我们希望以下代码在没有重复的情况下工作

C#中的伪代码

//get first 10 records that are not processed based on some other conditions
var recordSet = objectontext.recordstable.Where(...).Where(c => c.IsProcessed == false).Take(10);
//loop through the recordset in a transaction 
foreach(record singleRecord in recordSet)
{
    bool result = ProcessRecord();
    //Mark isProcessed as true 
    if(result)
        singleRecord.IsProcessed = true;
    objectContext.Savechanges();
}

我们希望避免重复处理记录(因为ProcessRecords()包含邮件程序等)。 如果我们将上面的整个代码包装成 交易是否意味着来自两个不同工作人员的两次调用会导致非重复记录?

如果workerA首先发出对表的调用,

var recordSetWorkerA = objectontext.recordstable.Where(somecondition...).Where(c => c.IsProcessed == false).Take(10);

如果workerB在工作者A已经处于事务中之后发出调用,则以下语句将无法执行,因为尝试读取锁定的行  或者转到接下来的10条记录?

var recordSetWorkerB = objectontext.recordstable.Where(somecondition...).Where(c => c.IsProcessed == false).Take(10);

我们应该关注哪种模式。

2 个答案:

答案 0 :(得分:1)

一个选项是明确地使isProcessed成为{ready,processing,processed}的三态枚举。我不知道如何在ActiveRecord中执行此操作,但您需要一个SQL语句,如:

UPDATE RecordsTable
SET ProcessedState = 'processing'
WHERE RecordId = 1
    AND ProcessedState = 'ready';

确保此语句只更新了一行。如果是零行,有人会打败你完成那项任务。确保此语句在其自己的事务中至少执行“read committed”隔离级别。

答案 1 :(得分:1)

将代码包装到事务中是不够的。你当然会在SaveChanges上获得例外,但为时已晚。

您真正需要的是将记录标记为正在处理,而不仅仅是已完成处理。我看到两个解决方案:

  1. 如果您的工作人员共享相同的状态(意味着他们是一个AppDomain中的线程,而不是几个并发的工作服务),您可以使用ConcurrentDictionary来标记您正在处理的记录。

    foreach(record singleRecord in recordSet)
    {
        //RecordsInProcess is a globally-available ConcurrentDictionary<recordIdType, record
        if (!RecordsInProcess.TryAdd(singleRecord.RecordId, singleRecord))
           continue; //TryAdd will return false if such an element already exists
    
        bool result = ProcessRecord();
        //Mark isProcessed as true 
        if(result)
            singleRecord.IsProcessed = true;
        objectContext.Savechanges();
        record junk; // we don't need it
        RecordsInProcess.TryRemove(singleRecordId, out junk)
    }
    
  2. 如果您的工作人员是孤立的,或者您只是想要更强大的东西,那么您必须将记录标记为在数据库中处理并使用该信息进行过滤。这就是你必须使用事务的地方,并且非常小心地使用它们,因为它很容易陷入僵局。从并发的角度来看,最有效的方法是始终只从数据库中获取一条未处理的记录,在执行任何操作之前将其标记为processing,然后继续处理。