检查行是否存在后发生主键冲突

时间:2017-12-11 17:14:40

标签: sql-server sql-server-2016

问题:什么事情可能导致以下问题?

我们有一个存储过程,用于将价格记录更新或插入表格中。很直接。但是,在间歇性的时间,我们会尝试插入已存在的行时发生主键冲突。如您所见,我们正在检查是否存在记录,然后根据需要进行更新或插入。

这种情况很少发生,但最后一次发生时我们在3分钟的窗口内发生了18次。我们更新了比18更多的行,所以不是每次都有。这个SP经常被调用。我们检查并且当时没有进行索引维护。调用此SP的应用程序只是循环通过队列来更新/插入这些价格,并且只有一个应用程序实例正在运行。

这是在具有3台服务器的2016年可用性组上运行。

ALTER PROCEDURE [dbo].[mw_UpdatePrice] @CustTypeID INT
    ,@ID INT
    ,@Price MONEY
    ,@OldPrice MONEY
    ,@ExpirationDate DATETIME
    ,@PriceStatusID INT
    ,@PriceStatusDesc VARCHAR(80)
    ,@FreeFreightShipviaServiceLevelID INT = NULL
    ,@FreeFreightShipViaServiceLevelDescription VARCHAR(150) = NULL
    ,@FreeFreightShipViaServiceLevelRank INT = NULL
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (
            SELECT 1
            FROM dbo.Price
            WHERE ID = @ID
                AND CustTypeID = @CustTypeID
            )
    BEGIN
        UPDATE dbo.Price
        SET Price = @Price
            ,OldPrice = @OldPrice
            ,ExpirationDate = @ExpirationDate
            ,PriceStatusID = @PriceStatusID
            ,PriceStatusDescription = @PriceStatusDesc
            ,ServiceLevelName = @FreeFreightShipViaServiceLevelDescription
            ,ServiceLevelId = @FreeFreightShipviaServiceLevelID
            ,ServiceLevelRank = @FreeFreightShipViaServiceLevelRank
        WHERE ID = @ID
            AND CustTypeID = @CustTypeID
    END
    ELSE
    BEGIN
        INSERT dbo.Price (
            ID
            ,CustTypeID
            ,Price
            ,OldPrice
            ,ExpirationDate
            ,PriceStatusID
            ,PriceStatusDescription
            ,ServiceLevelName
            ,ServiceLevelID
            ,ServiceLevelRank
            )
        VALUES (
            @ID
            ,@CustTypeID
            ,@Price
            ,@OldPrice
            ,@ExpirationDate
            ,@PriceStatusID
            ,@PriceStatusDesc
            ,@FreeFreightShipViaServiceLevelDescription
            ,@FreeFreightShipviaServiceLevelID
            ,@FreeFreightShipViaServiceLevelRank
            )
    END
END

1 个答案:

答案 0 :(得分:1)

正如评论所提到的,这个存储过程并不是安全的并发调用 - 它具有插入和更新方案的竞争条件。这可以通过确保对存储过程的每次调用都包含在单独的事务中(在SP中添加begin / end tran,或从应用程序代码启动事务),并将HOLDLOCK应用于SELECT和INSERT语句:

SELECT 1
FROM dbo.Price WITH (UPDLOCK, HOLDLOCK)
WHERE ID = @ID
    AND CustTypeID = @CustTypeID

...

INSERT dbo.Price WITH (HOLDLOCK)
...

即使将其重构为MERGE语句,仍然需要HOLDLOCK来防止此问题。示例here

HOLDLOCK会锁定整个表,并会降低此SP的并发吞吐量。

除了并发问题之外,如果ANSI_NULLS打开,可能会发生PRIMARY KEY违规的另一个可能原因,可能允许应用程序逻辑错误,其中使用@CustTypeID或@ID的NULL值调用SP,导致应用程序以一种非预期的方式行事。例如,使用@CustTypeID = NULL和@ID = 1调用将始终导致INSERT,这可能不是预期的行为。

修改

根据@DavidBrowne

添加UPDLOCK