实体框架可以在高并发添加的情况下分配错误的标识列值

时间:2013-09-26 22:09:06

标签: entity-framework concurrency insert

我们有一个自动增量标识列Id作为我的用户对象的一部分。对于我们刚刚为客户开展的广告系列,我们每分钟最多注册600次。这是执行添加的代码块:

using (var ctx = new {{ProjectName}}_Entities())
{
    int userId = ctx.Users.Where(u => u.Email.Equals(request.Email)).Select(u => u.Id).SingleOrDefault();
    if (userId == 0)
    {
        var user = new User() { /* Initializing user properties here */ };
        ctx.Users.Add(user);
        ctx.SaveChanges();
        userId = user.Id;
    }
    ...
}

然后我们使用userId将数据插入另一个表中。在高负载期间发生的事情是,有多个行具有相同的userId,即使不应该。看起来上面的代码为多个插入返回了相同的Identity(int)编号。

我通读了一些博客/论坛帖子,说明SCOPE_IDENTITY()可能存在问题,实体框架在插入后会使用该问题返回自动增量值。

他们说可能的解决方法是为我熟悉的INSERT ... OUTPUT INSERTED.Id用户编写插入程序。

还有其他人遇到过这个问题吗?关于如何使用Entity Framework处理这个问题的任何建议?

更新1:

在进一步分析数据后,我几乎100%肯定这是问题所在。标识列已跳过自动增量值的48倍2727, (2728 missing), 2729,...,而在另一个表中我们只有48个重复。

似乎EF为每个因某些原因无法插入的行返回了随机的Identity值。

任何人都知道这可能会发生什么?

更新2:

我没有提到的可能重要的信息是,这发生在使用Azure SQL的Azure网站上。我们在发生这种情况时运行了4个实例。

更新3:

存储过程:

CREATE PROCEDURE [dbo].[p_ClaimCoupon]
    @CampaignId int,
    @UserId int,
    @Flow tinyint
AS

DECLARE @myCoupons TABLE
(
    [Id] BIGINT NOT NULL,
    [Code] CHAR(11) NOT NULL, 
    [ExpiresAt] DATETIME NOT NULL,
    [ClaimedBefore] BIT NOT NULL
)

INSERT INTO @myCoupons
SELECT TOP(1) c.Id, c.Code, c.ExpiresAt, 1
FROM Coupons c
WHERE c.CampaignId = @CampaignId AND c.UserId = @UserId

DECLARE @couponCount int = (SELECT COUNT(*) FROM @myCoupons)
IF @couponCount > 0
BEGIN
    SELECT *
    FROM @myCoupons
END
ELSE
BEGIN
    UPDATE TOP(1) Coupons
    SET UserId = @UserId, IsClaimed = 1, ClaimedAt = GETUTCDATE(), Flow = @Flow
    OUTPUT DELETED.Id, DELETED.Code, DELETED.ExpiresAt, CAST(0 AS BIT) as [ClaimedBefore]
    WHERE CampaignId = @CampaignId AND IsClaimed = 0
END

RETURN 0

从同一个EF上下文中调用这个:

var coupon = ctx.Database.SqlQuery<CouponViewModel>(
    "EXEC p_ClaimCoupon @CampaignId, @UserId, @Flow",
    new SqlParameter("CampaignId", {{CampaignId}}),
    new SqlParameter("UserId", {{userId}}),
    new SqlParameter("Flow", {{Flow}})).FirstOrDefault();

3 个答案:

答案 0 :(得分:2)

不,那是不可能的。首先,这将是EF中一个令人震惊的错误。你不是第一个将600插入/秒放在上面的人。此外,SCOPE_IDENTITY明确是安全的,是推荐的做法。

这些语句适用于您使用SQL Server IDENTITY列作为ID。

答案 1 :(得分:0)

我承认我不知道Azure SQL数据库如何同步独特的顺序ID的生成,但直观地说它必须是昂贵的,特别是按照您的费率。

如果非顺序ID是一个选项,您可能需要考虑在应用程序级别生成UUID。我知道这不能回答你的直接问题,但它会提高性能(未经验证)并绕过你的问题。

更新:说,Azure SQL数据库未分发,只是replicated from a single primary node。因此,IDENTITY密钥替代品不会带来真正的性能提升,并且假设实例数量对您的问题并不重要。

答案 2 :(得分:0)

我认为你的问题可能在这里:

UPDATE TOP(1) Coupons
SET UserId = @UserId, IsClaimed = 1, ClaimedAt = GETUTCDATE(), Flow = @Flow
OUTPUT DELETED.Id, DELETED.Code, DELETED.ExpiresAt, CAST(0 AS BIT) as [ClaimedBefore]
WHERE CampaignId = @CampaignId AND IsClaimed = 0

这将更新它在尚未声明的广告系列中找到的第一条记录的UserId。在插入用户失败的情况下,它看起来不健壮。你确定这是正确的吗?