如何在多任务环境中防止重复插入azure表?

时间:2014-06-06 04:26:05

标签: c# multithreading azure

我有一个多任务应用程序,其中多个任务同时运行。每个任务检查一个recordId是否已经存在于azure表中。如果没有,它会添加它。我的问题是,虽然我已经应用了对recordId的检查,但仍然会添加重复的条目。

public async Task<bool> TryExecuteAsync(ServiceCommandMessage commandMessage, CancellationToken token, IProgress<string> progress)
        {
            token.ThrowIfCancellationRequested();
            var isSuccessful = true;

            return await System.Threading.Tasks.Task.Run(() =>
            {
                token.ThrowIfCancellationRequested();

                var watch = new Stopwatch();
                watch.Start();

                try
                {
                    StoreFourSqaureMetadata(id);
                }

                catch (Exception ex)
                {                    
                    isSuccessful = false;
                    throw ex;
                }

                watch.Stop();

                return isSuccessful;
            }, token);
        }
public static void StoreFourSqaureMetadata(string Id)
    {
        var noDataAvailable = "No data available".Trim();
        try
        {               
            var d = IsExist(Id); //Checking if Id already exist in Table
            if (d != null) return;
            //If not add to table

        }
    }

2 个答案:

答案 0 :(得分:2)

我认为对您的问题的最佳解决方案有两个相当不言自明的部分:(1)在表中的相应列上创建唯一键; (2)插入失败后捕获错误。

唯一的关键是真正重要的部分。这是确保不发生此类事情的唯一方法,因为数据库是您的架构中唯一能够保证这种一致性的部分。

在可能存在问题的地方,我会使用这样的模式。首先,我有一组辅助方法可以帮助我重试:

/// <summary>
/// Try a given async action 'n' times or until it succeeds.
/// </summary>
/// <param name="times">The number of times to retry the action</param>
/// <param name="action">The action to retry</param>
/// <param name="pauseInMilliseconds">The amount of time in milliseconds to pause between retries (defaults to 0)</param>
public async static Task<T> RetriesAsync<T>(this int times, Func<int, Task<T>> action, int pauseInMilliseconds)
{
    var attempt = 0;
    var result = default(T);
    while (attempt < times)
    {
        try
        {
            result = await action(attempt);
            break;
        }
        catch (Exception)
        {
            attempt++;
            if (attempt >= times)
            {
                throw;
            }
        }
        if (pauseInMilliseconds > 0)
        {
            await Task.Delay(pauseInMilliseconds);
        }
    }
    return result;
}

然后我有方法检查行是否存在;如果是的话,它会返回它;如果没有,则插入然后返回。这有点像这样:

private async Task<Customer> CreateOrGetCustomer(IEntities db, int customerId)
{
    var customer = await db.Customers.FirstOrDefaultAsync(x => x.CustomerId == customerId);
    if (customer == null)
    {
        customer = new Customer { CustomerId = customerId };
        db.Customers.Add(customer);
        await db.SaveChangesAsync();
    }
    return customer;
}

然后我用这样的重试调用该方法:

var customer = await 2.RetriesAsync(async x => CreateOrGetCustomer(db, customerId));

我确信有更优雅的方法可以做到这一点,但它确实有效 - 至少,如果您已经在桌面上配置了所有相应的唯一键,它就会起作用。

认为这两个部分是相当不言自明的,但如果你需要更多的指导,或者由于某些原因它们不适合你,请告诉我。

答案 1 :(得分:0)

这是一个常见的问题,称为Race Condition,它们可能特别麻烦,尤其是当您处理数据库时。

当两个(或更多)线程试图同时添加相同的ID值时,问题就出现了。他们都检查数据库中的表以查看ID是否存在,两者都发现它没有,然后都为它添加一条新记录。

有很多方法可以使它工作:在检查和插入时锁定表的存储过程,ID字段上的唯一键或索引以强制多次插入尝试失败,单个线程负责插入,一个线程安全的插入ID集合,您可以检查并插入锁定等。您选择哪种方法在很大程度上取决于您的应用程序的要求。

如果您不担心直接将数据导入数据库会有一些延迟,您可以在StoreFourSqaureMetadata方法中使用锁定来确保一次只有一个线程正在更新数据库:

private static readonly object _lock = new object();

public static void StoreFourSqaureMetadata(string Id)
{
    var noDataAvailable = "No data available".Trim();
    lock(_lock)
    {
        try
        {               
            var d = IsExist(Id); //Checking if Id already exist in Table
            if (d != null) 
                return;
            //If not add to table
        }
        catch { }
    }
}

这绝对会阻止两个线程同时尝试添加记录,但代价是让所有操作排队并一次运行一个代码。它将阻止线程对相同数据进行多次插入,但最终会降低整体吞吐量。

如果没有关于您的具体问题的更多信息,我无法真正建议更具体的解决方案。例如,如果您总是获得新ID并且不必关心数据库中已有的ID,那么您可以在内存中维护一个列表并且只锁定足够长的时间以检查并在该列表中插入条目...无穷无尽:P