【问题标题】:TSQL implementing double check lockingTSQL 实现双重检查锁定
【发布时间】:2013-04-30 11:26:21
【问题描述】:

我有一个任意的存储过程usp_DoubleCheckLockInsert,它为多个客户端执行INSERT,当它位于临界区Begin lockEnd lock.

CREATE PROCEDURE usp_DoubleCheckLockInsert
     @Id INT
    ,@SomeValue INT
AS
BEGIN
    IF (EXISTS(SELECT 1 FROM SomeTable WHERE Id = @Id AND SomeValue = @SomeValue)) RETURN
    BEGIN TRAN
        --Begin lock
        IF (EXISTS(SELECT 1 FROM SomeTable WHERE Id = @Id AND SomeValue = @SomeValue)) ROLLBACK

        INSERT INTO SomeTable(Id, SomeValue)
        VALUES(@Id,@SomeValue);
        --End lock
    COMMIT
END

我已经看到隔离级别与更新的关系,但是有没有办法在关键部分实现锁定,给事务写锁,或者 TSQL 不是这样工作的?

Obtain Update Table Lock at start of Stored Procedure in SQL Server

【问题讨论】:

  • 为什么不在Id, SomeValue 上创建一个唯一约束?见Only inserting a row if it's not already there
  • 这是在数据级别解决问题的一种方法,但在我的情况下,某些记录有一个名为IsDelete 的软删除标志,因此相同IdSomeValue 的多个记录是允许的。
  • 您可以使用唯一的过滤索引来强制执行此操作。 CREATE UNIQUE INDEX IX ON SomeTable(Id,SomeValue) WHERE IsDelete = 0
  • 如果这是用户可以访问此表的唯一方式,您可能需要查看 sp_getapplock msdn.microsoft.com/en-us/library/ms189823.aspx
  • "这真的比双重检查锁定更可取吗?" - 是的,是的。在您经历了所有的扭曲之后,SQL Server 仍将应用所有正常的锁定机制。在这种情况下,捕获错误首选的工作方式。

标签: sql sql-server-2008 stored-procedures concurrency locking


【解决方案1】:

对我有用的第二种方法是将INSERTSELECT 组合成一个操作。

这个索引只需要高效查询SomeTable。请注意,没有唯一性约束。但是,如果我采用这种方法,我实际上会使索引唯一。

CREATE INDEX [IX_SomeTable_Id_SomeValue_IsDelete] ON [dbo].[SomeTable]
(
    [Id] ASC,
    [SomeValue] ASC,
    [IsDelete] ASC
)

存储过程,结合了 INSERT/SELECT 操作:

CREATE PROCEDURE [dbo].[usp_DoubleCheckLockInsert]
     @Id INT
    ,@SomeValue INT
    ,@IsDelete bit
AS
BEGIN

        -- Don't allow dirty reads

        SET TRANSACTION ISOLATION LEVEL READ COMMITTED

    BEGIN TRAN
        -- insert only if data not existing
        INSERT INTO dbo.SomeTable(Id, SomeValue, IsDelete)
        SELECT @Id, @SomeValue, @IsDelete
        where not exists (
            select * from dbo.SomeTable WITH (HOLDLOCK, UPDLOCK)
            where Id = @Id
            and SomeValue = @SomeValue
            and IsDelete = @IsDelete)

    COMMIT
END

我确实尝试过使用多个进程插入数据的这种方法。 (尽管我承认我并没有对 SQL Server 施加太大压力)。从来没有任何重复或失败的插入。

【讨论】:

  • 你的两个答案都是正确的。我会接受你感觉更好的遵循 SQL 约定的那个。
  • @Romoku - 对我来说他们都一样好。也许您应该稍等一下,看看哪个获得更多选票。当然,对于这个答案,我会创建一个唯一索引(正如我在上面的答案中建议的那样)。
  • @Romoku - 经过进一步测试,我发现我的原始 SQL 偶尔会失败,因为它试图插入已经存在的记录。解决此问题的方法是在子选择中添加表格提示(holdlockupdlock)。 holdlock 等价于serializable,单线程都可以访问。此外,根据 MSDN,如果有表提示,则这些表的任何 TRANSACTION ISOLATION LEVEL 语句都将被忽略。我已更新我的答案以反映我的发现。
【解决方案2】:

看来您要做的只是防止插入重复的行。你可以通过添加一个唯一索引来做到这一点,选项IGNORE_DUP_KEY = ON

CREATE UNIQUE INDEX [IX_SomeTable_Id_SomeValue_IsDelete]
ON [dbo].[SomeTable]
(
    [Id] ASC,
    [SomeValue] ASC,
    [IsDelete] ASC
) WITH (IGNORE_DUP_KEY = ON)

SQL Server 将忽略任何具有重复键的插入。运行以下命令:

INSERT INTO [dbo].[SomeTable] ([Id],[SomeValue],[IsDelete])
VALUES(0,0,0)

INSERT INTO [dbo].[SomeTable] ([Id],[SomeValue],[IsDelete])
VALUES(1,1,0)

INSERT INTO [dbo].[SomeTable] ([Id],[SomeValue],[IsDelete])
VALUES(2,2,0)

INSERT INTO [dbo].[SomeTable] ([Id],[SomeValue],[IsDelete])
VALUES(0,0,0)

结果:

(1 row(s) affected)

(1 row(s) affected)

(1 row(s) affected)

Duplicate key was ignored.
(0 row(s) affected)

我没有使用多个进程(线程)测试上述内容,但这种情况下的结果应该是相同的 - SQL Server 仍然应该忽略任何重复项,无论哪个线程正在尝试插入。

另请参阅 MSDN 上的 Index Options

【讨论】:

    【解决方案3】:

    我想我可能不明白这个问题,但你为什么不能这样做:

    begin tran
    if ( not exists ( select 1 from SomeTable where Id = @ID and SomeValue = @SomeValue ) )
        insert into SomeTable ( Id, SomeValue ) values ( @ID, @SomeValue )
    commit
    

    是的,您每次执行此操作时都会进行交易,但只要您的速度很快,这应该不是问题。

    我感觉我没有理解这个问题。

    杰夫。

    【讨论】:

    • 但是如果你是并发运行,难道其他人不应该等到你释放提交的事务吗?
    【解决方案4】:

    一旦你开始搞乱 sql 首选锁定管理,你就会承担责任,但如果你确定这是你需要的,请更新你的 sp 以选择一个测试变量并将你的“EXISTS”检查替换为那个变量。当您查询变量时,请使用排他表锁,在您完成之前,该表是您的。

    CREATE PROCEDURE usp_DoubleCheckLockInsert
         @Id INT
        ,@SomeValue INT
    AS
    BEGIN
         IF (EXISTS(SELECT 1 FROM SomeTable WHERE Id = @Id AND SomeValue = @SomeValue)) RETURN
         BEGIN TRAN
            --Begin lock
             DECLARE @tId as INT
             -- You already checked and the record doesn't exist, so lock the table
             SELECT @tId 
             FROM SomeTable WITH (TABLOCKX)
             WHERE Id = @Id AND SomeValue = @SomeValue
             IF @tID IS NULL
               BEGIN
                 -- no one snuck in between first and second checks, so commit
               INSERT INTO SomeTable(Id, SomeValue)
               VALUES(@Id,@SomeValue);
            --End lock
         COMMIT
    END
    

    如果您将其作为查询执行,但未命中提交,然后尝试从不同上下文的表中进行选择,您将坐下来等待提交生效。

    【讨论】:

      【解决方案5】:

      Romoku,你得到的答案基本上是正确的,除了

      • 甚至不需要BEGIN TRAN
      • 您无需担心隔离级别。

      您只需要一个简单的insert ... select ... where not exists (select ...),正如 Jeff B 和 Chue X 所建议的那样。

      您对并发性的担忧(“我说的是并发性,而您的回答不起作用。”)揭示了对 SQL 工作原理的深刻误解。

      SQL INSERT 原子的。您不必锁定桌子;这就是 DBMS 为您所做的。

      我建议不要为基于错误的先入为主的观念而提出的错误问题提供奖励——然后将正确的答案视为错误的答案——我建议坐下来阅读一本好书。在 SQL 上。如果你喜欢,我可以推荐一些标题。

      【讨论】:

      • Jeff 从未建议过insert ... select ... where not exists (select ...) 可行。你说得对,我没有意识到 INSERT 是原子的,所以将重复检查逻辑结合到 INSERT 语句中是我正在寻求的主动方法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-08
      相关资源
      最近更新 更多