Parallel.ForEach loop 在C#中只允许一个线程访问DB

Parallel.ForEach loop Allow only one thread to access DB in C#

我有一段代码,其中多个线程使用来自 ConcurrentBag 类型字符串的共享 ID 属性 进行访问,如下所示:

var ids = new ConcurrentBag<string>();
// List contains lets say 10 ID's 
var apiKey = ctx.ApiKey.FirstOrDefault();

Parallel.ForEach(ids, id => 
{
    try
    {
        // Perform API calls
    }
    catch (Exception ex)
    {
        if (ex.Message == "Expired")
        {
            // the idea is that if that only one thread can access the DB record to update it, not multiple ones
            using (var ctx = new MyEntities())
            {
                var findApi= ctx.ApiKeys.Find(apiKey.RecordId);
                findApi.Expired = DateTime.Now.AddHours(1);
                findApi.FailedCalls += 1;
            }
        }
    }

});

所以在这种情况下,如果我有一个用于 API 调用的 10 个 ID 和 1 个密钥的列表,一旦密钥达到每小时调用限制,我将捕获异常API 然后标记下一个小时不使用的密钥。

但是,在我上面粘贴的代码中,所有 10 个线程都将从数据库访问记录并将失败的调用计为 10 次,而不是仅 1..:/

所以我的问题是如何防止所有线程更新数据库记录,而是只允许一个线程访问数据库,更新记录(通过 +1 添加失败的调用) ?

我怎样才能做到这一点?

看来只要出错就更新apiKey.RecordId一次,为什么不跟踪出错的事实,最后更新一次呢?例如

var ids = new ConcurrentBag<string>();
// List contains lets say 10 ID's 
var apiKey = ctx.ApiKey.FirstOrDefault();
var expired = false;

Parallel.ForEach(ids, id => 
{
    try
    {
        // Perform API calls
    }
    catch (Exception ex)
    {
        if (ex.Message == "Expired")
        {
           expired = true;
        }
    }
}

if (expired)
{
   // the idea is that if that only one thread can access the DB record to 
   // update it, not multiple ones
   using (var ctx = new MyEntities())
   {
     var findApi= ctx.ApiKeys.Find(apiKey.RecordId);
     findApi.Expired = DateTime.Now.AddHours(1);
     findApi.FailedCalls += 1;
    }
});

您处于并行循环中,因此最有可能的行为是 10 个线程中的每一个都将被触发,尝试使用过期密钥连接到您的 API 然后全部失败,抛出异常。

对此有几个合理的解决方案:

使用前检查密钥

能否让循环中的第一个 运行 乱序?例如:

var ids = new ConcurrentBag<string>();
var apiKey = ctx.ApiKey.FirstOrDefault();

bool expired = true;

try {
  // Perform API calls
  expired = false;
}
catch(Exception ex) {
   // log to database once
}

// Or grab another, newer key?
if (!expired)
{
  Parallel.ForEach(ids.Skip(1), id => 
  {
     // Perform API Calls
  }
}

如果密钥在您使用它之前可能已经过期,但在您使用它时将处于活动状态,那么这将工作得很好。

坚持失败

如果密钥在您开始时可能有效,但在您使用它时可能会过期,您可能想尝试捕获该失败,然后在结束时记录。

var ids = new ConcurrentBag<string>();
var apiKey = ctx.ApiKey.FirstOrDefault();

// Assume the key hasn't expired - don't set to false within the loops 
bool expired = false;

Parallel.ForEach(ids.Skip(1), id => 
{
  try {
     // Perform API calls
  }
  catch (Exception e) {
    if (e.Message == "Expired") {
      // Doesn't matter if many threads set this to true.
      expired = true;
    }
  }

  if (expired) {
    // Log to database once.
  }
}