【问题标题】:Primary Key violation after checking for existence of row检查行是否存在后主键违规
【发布时间】:2017-12-11 17:14:39
【问题描述】:

问题:哪些因素会导致以下问题?

我们有一个存储过程,用于更新或将价格记录插入表中。非常直截了当。但是,有时我们会在尝试插入已经存在的行时遇到主键冲突。如您所见,我们正在检查记录是否存在,然后酌情更新或插入。

这种情况很少发生,但上一次发生时,我们在 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

【问题讨论】:

  • 你的主键是什么?是IDCustTypeID的复合键吗?您可以将表格的 DDL 编辑到您的帖子中吗?如果它不是复合键,那就是你的问题;您正在检查特定IDCustTypeID 的条目是否存在,因此如果ID 存在但对于不同的CustTypeID,您将获得违规。
  • 首先你必须在一个事务中做这种工作。第二:这类问题最适合使用 MERGE 语句。在这里查看red-gate.com/simple-talk/sql/learn-sql-server/…
  • @Larnu 这就是PK。我仔细检查了。
  • @SteveB 是什么?就ID?
  • @MihailShishkov 这已经存在了一段时间,所以我不知道为什么它使用它的结构。我确实记得在奇怪的情况下 MERGE 存在一些问题,所以这可能就是原因。我打赌这笔交易可能会解决问题。遗憾的是我没有想到这一点。

标签: sql-server sql-server-2016


【解决方案1】:

正如 cmets 所提到的,这个存储过程并发调用是不安全的 - 它对于插入和更新场景都有竞争条件。只能通过确保对存储过程的每次调用都包含在单独的事务中(在 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

【讨论】:

  • HOLDLOCK 对于 SELECT 来说是不够的。两个会话都将获得一个 S Range 锁,然后在 INSERT 上死锁。您需要 (UPDLOCK,HOLDLOCK) 在这里。使用 MERGE 不需要 UPDLOCK,因为 MERGE 使用 U 锁读取,就像 UPDATE 一样。但是 SELECT 默认会用 S 锁读取
  • @DanLing 我唯一的问题是,据我们所知,这个 SP 没有被同时调用。
  • 我唯一能想到的另一件事是,SP 的调用者可能正在为主键列之一发送空值。如果 Id 或 CustTypeID 为空,您可以向此 SP 添加验证以引发错误。
  • @DanLing 幸运的是,在我们的错误日志中,我们有实际数据并且它不为空。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-02
  • 1970-01-01
  • 2011-04-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多