避免重复使用同一数据库表的并发读取
我们有一个包含任务列表的表
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);
我们应该关注哪种模式。
答案 0 :(得分:1)
一个选项是明确地使isProcessed成为{ready,processing,processed}的三态枚举。我不知道如何在ActiveRecord中执行此操作,但您需要一个SQL语句,如:
UPDATE RecordsTable
SET ProcessedState = 'processing'
WHERE RecordId = 1
AND ProcessedState = 'ready';
确保此语句只更新了一行。如果是零行,有人会打败你完成那项任务。确保此语句在其自己的事务中至少执行“read committed”隔离级别。
答案 1 :(得分:1)
将代码包装到事务中是不够的。你当然会在SaveChanges
上获得例外,但为时已晚。
您真正需要的是将记录标记为正在处理,而不仅仅是已完成处理。我看到两个解决方案:
如果您的工作人员共享相同的状态(意味着他们是一个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)
}
processing
,然后继续处理。