防止MS-SQL中的条件INSERT / UPDATE竞争条件

时间:2017-05-09 11:50:07

标签: sql sql-server conditional sql-server-2016

我想知道我是否遵循正确的方法并需要你的帮助来弄明白

这是我的非受保护查询

DECLARE @cl_WordId bigint = NULL
SELECT
  @cl_WordId = cl_WordId
FROM tblWords
WHERE cl_Word = @cl_Word
AND cl_WordLangCode = @cl_WordLangCode
IF (@cl_WordId IS NULL)
BEGIN
  INSERT INTO tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
    VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)
  SET @cl_WordId = SCOPE_IDENTITY()
  SELECT
    @cl_WordId
END
ELSE
BEGIN
  SELECT
    @cl_WordId
END

为了保护它,我将其修改如下

DECLARE @cl_WordId bigint = NULL
SELECT
  @cl_WordId = cl_WordId
FROM tblWords WITH (HOLDLOCK)
WHERE cl_Word = @cl_Word
AND cl_WordLangCode = @cl_WordLangCode
BEGIN
  IF (@cl_WordId IS NULL)
  BEGIN
    INSERT INTO tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
      VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)
    SET @cl_WordId = SCOPE_IDENTITY()
    SELECT
      @cl_WordId
  END
  ELSE
  BEGIN
    SELECT
      @cl_WordId
  END
END

所以我已将WITH (HOLDLOCK)添加到选择查询中,并将beginend添加到选择查询

此方法是否正确以防止条件INSERT / UPDATE竞争条件

1 个答案:

答案 0 :(得分:1)

我在上一个问题(Conditional INSERT/UPDATE Race Condition“UPSERT” Race Condition With MERGE)中使用MERGEHOLDLOCK发布的文章中提到的是线程安全的,因此您的查询将是:

MERGE tblWords WITH (HOLDLOCK) AS w
USING (VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)) AS s (cl_Word, cl_WordLangCode, cl_SourceId)
    ON s.cl_Word = w.cl_Word
    AND s.cl_WordLangCode = w.cl_WordLangCode
WHEN NOT MATCHED THEN 
    INSERT (cl_Word, cl_WordLangCode, cl_SourceId)
    VALUES (s.cl_Word, s.cl_WordLangCode, s.cl_SourceId);

看起来这可能是一个存储过程,并且您正在使用SELECT @cl_WordId将ID返回给调用者。这属于Aaron Bertrand's bad habits to kick之一,而应使用输出参数,例如:

CREATE PROCEDURE dbo.SaveCLWord
        @cl_Word            VARCHAR(255), 
        @cl_WordLangCode    VARCHAR(255), 
        @cl_SourceId        INT,
        @cl_WordId          INT OUTPUT
AS
BEGIN

    MERGE tblWords WITH (HOLDLOCK) AS w
    USING (VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)) AS s (cl_Word, cl_WordLangCode, cl_SourceId)
        ON s.cl_Word = w.cl_Word
        AND s.cl_WordLangCode = w.cl_WordLangCode
    WHEN NOT MATCHED THEN 
        INSERT (cl_Word, cl_WordLangCode, cl_SourceId)
        VALUES (s.cl_Word, s.cl_WordLangCode, s.cl_SourceId);

    SELECT  @cl_WordId = w.cl_WordId
    FROM    tblWords AS w
    WHERE   s.cl_Word = @cl_Word
    AND     s.cl_WordLangCode = @cl_WordLangCode;

END

<强> ADDEDNUM

您可以在没有MERGE的情况下执行此操作,如下所示。

BEGIN TRAN

INSERT tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
SELECT  @cl_Word, @cl_WordLangCode, @cl_SourceId
WHERE   NOT EXISTS
        (   SELECT  1
            FROM    tblWords WITH (UPDLOCK, HOLDLOCK)
            WHERE   cl_Word = @cl_Word
            AND     l_WordLangCode = @cl_WordLangCode
        );

COMMIT TRAN;

SELECT  @cl_WordId = w.cl_WordId
FROM    tblWords AS w
WHERE   s.cl_Word = @cl_Word
AND     s.cl_WordLangCode = @cl_WordLangCode;

如果你没有使用合并,因为你是concerned about its bugs,或者因为在这种情况下你实际上没有UPDATE,那么MERGE是过度杀手而且是{{ 1}}就足够了,那就足够了。但是没有使用它,因为它是不熟悉的语法不是最好的理由,花时间阅读它,了解更多,并在SQL弓中添加另一个字符串。

修改

来自online docs

<强> HOLDLOCK

  

相当于SERIALIZABLE。有关更多信息,请参阅本主题后面的SERIALIZABLE。 HOLDLOCK仅适用于指定了它的表或视图,仅适用于中使用的语句定义的事务的持续时间。 HOLDLOCK不能在包含FOR BROWSE选项的SELECT语句中使用。

因此,在您的查询中,您有6个语句:

INSERT

由于您没有显式事务,因此每个语句都在其自己的隐式事务中运行,因此专注于语句2,这相当于:

-- STATETMENT 1
DECLARE @cl_WordId bigint = NULL

--STATEMENT 2
SELECT
  @cl_WordId = cl_WordId
FROM tblWords WITH (HOLDLOCK)
WHERE cl_Word = @cl_Word
AND cl_WordLangCode = @cl_WordLangCode

BEGIN

--STATEMENT 3
  IF (@cl_WordId IS NULL)
  BEGIN

    -- STATEMENT 4
    INSERT INTO tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
      VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)
    SET @cl_WordId = SCOPE_IDENTITY()

    --STATEMENT 5
    SELECT
      @cl_WordId
  END
  ELSE
  BEGIN

    -- STATEMENT 6
    SELECT
      @cl_WordId
  END
END

因此,由于BEGIN TRAN SELECT @cl_WordId = cl_WordId FROM tblWords WITH (HOLDLOCK) WHERE cl_Word = @cl_Word AND cl_WordLangCode = @cl_WordLangCode COMMIT TRAN 适用于使用它的事务的持续时间,因此锁定被释放,一旦此代码完成就会释放锁定,因此当您进入语句3时另外一个线程可以插入到表中。