1

避免对同一个数据库表的并发读取重复

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

Table RecordsTable  
    RecordID
    RecordName
    ...
    ...
    IsProcessed

多台工作机器从表中读取,一旦任务被处理,将 IsProcessed 标记为 true。

因此,如果我们希望以下代码可以正常工作而不会重复

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 在worker A 已经在事务中之后发出调用,以下语句是否会因为试图读取锁定的行或移动到接下来的10 条记录而无法执行?

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

有没有我们应该关注的模式。

4

2 回答 2

1

一种选择是显式地使 isProcessed 成为 { 就绪、处理、处理 } 的三态枚举。我不知道如何在 ActiveRecord 中执行此操作,但您需要如下 SQL 语句:

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

确保该语句只更新了一行。如果是零行,那么有人会击败您完成该任务。确保该语句在其自己的事务中执行,至少具有“读提交”隔离级别。

于 2012-07-28T13:01:11.300 回答
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在您执行任何操作之前,然后继续您的处理。
于 2012-07-28T13:08:15.783 回答