行可能不存在时如何进行原子递增?

时间:2015-05-18 23:46:49

标签: c# .net entity-framework entity-framework-6

假设我MyContext派生DbContext DbSet<Item> Items,其中Item的定义如下。

public class Item
{
    [Key]
    public string Key { get; set; }

    [ConcurrencyCheck]
    public int Value { get; set; }
}

给定一个键,我想原子地递增相应的值,其中表中尚未存在的键的隐含值为0.也就是说,原子地递增表中不存在的键会导致值为1这是我目前的做法:

public static async Task IncrementAsync(string key)
{
    using (var context = new MyContext())
    {
        while (true)
        {
            var item = await context.Items.FindAsync(key);
            if (item == null)
            {
                context.Items.Add(new Item { Key = key, Value = 1 });
            }
            else
            {
                item.Value++;
            }
            try
            {
                await context.SaveChangesAsync();
                break;
            }
            catch (DbUpdateException)
            {
                continue;
            }
        }
    }
}

当许多对IncrementAsync的调用同时运行时,这会因为实时锁定而失败。

  • 这样做的正确方法是什么?
  • while循环是否应该在using之外,以便每次尝试都获得新的上下文?我尝试了这一点,它使一切正常,但我觉得我创造和破坏如此多的背景效率低下。
  • 我错过了关于上下文跟踪变化的方式吗?

我的实体框架体验基本上只是查询,所以如果你能解释一下我在这段代码中做错的细节,我真的很感激。

修改
因为所选答案没有明确说明,所以我会在这里放置更正后的代码。请注意context之后永远不会重复使用DbUpdateException

public static async Task IncrementAsync(string key)
{
    while (true)
    {
        using (var context = new MyContext())
        {
            var item = await context.Items.FindAsync(key);
            if (item == null)
            {
                context.Items.Add(new Item { Key = key, Value = 1 });
            }
            else
            {
                item.Value++;
            }
            try
            {
                await context.SaveChangesAsync();
                break;
            }
            catch (DbUpdateException)
            {
                continue;
            }
        }
    }
}

2 个答案:

答案 0 :(得分:2)

给定答案的替代方法是使用存储过程完成所有工作,如下例所示。然后,您可以在一行中从应用程序中调用它,而不是上面的代码。

CREATE PROCEDURE SP_IncrementValue
    @ValueKey NVARCHAR(100)
AS
BEGIN
    BEGIN TRAN
       UPDATE Item WITH (SERIALIZABLE) SET Value = Value + 1
       WHERE [Key] = @ValueKey

       IF @@ROWCOUNT = 0
       BEGIN
          INSERT Item ([Key], Value) VALUES (@ValueKey, 1)
       END
    COMMIT TRAN
END
GO

这种方法可以提供更好的性能,减少错误。

修改 要从C#调用存储过程,请在EF ApplicationDbContext类

中添加以下方法
    public int IncrementValue(string valueKey)
    {
        return this.Database.ExecuteSqlCommand("EXEC SP_IncrementValue @ValueKey", new SqlParameter("@ValueKey", valueKey));
    }

然后,您可以从DBContext类的任何实例中调用它

答案 1 :(得分:1)

您需要在尝试之间不共享上下文。如果没有对具有DbUpdateException的上下文做任何事情,就像你没有明确地清理上下文一样,它可能永远不会返回到正常状态。

我希望外部环境会引发问题。如果根据时间发生对单个键的并发调用,则可能会创建一个错误的上下文设置(由于您的错误处理程序,它会不断被忽略。

除非我弄错了,数据库中存在Key的事实不会删除“待添加”版本。您将最终得到以下其中一种情况:

Add "1", 2

Add "1", 1
Update "1", 2

取决于您的第二次迭代是否抓取第一个迭代的对象或新的对象。

这些都不会成功,所以最终会出现连续错误。