【问题标题】:Primary key conflict even when TABLOCKX and HOLDLOCK hints即使 TABLOCKX 和 HOLDLOCK 提示主键冲突
【发布时间】:2016-01-26 09:22:06
【问题描述】:

我有一个表,用于创建具有唯一键的锁,以控制在多个服务器上执行关键部分,即一次只有一个来自所有 Web 服务器的线程可以进入该关键部分。

锁机制首先尝试向数据库中添加一条记录,如果成功则进入该区域,否则等待。当它退出临界区时,它会从表中删除该键。为此,我有以下程序:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRANSACTION
  DECLARE @startTime DATETIME2
  DECLARE @lockStatus INT
  DECLARE @lockTime INT
  SET @startTime = GETUTCDATE()

  IF EXISTS (SELECT * FROM GuidLocks WITH (TABLOCKX, HOLDLOCK) WHERE Id = @lockName)
  BEGIN
    SET @lockStatus = 0
  END
  ELSE
  BEGIN
    INSERT INTO GuidLocks VALUES (@lockName, GETUTCDATE())
    SET @lockStatus = 1
  END

  SET @lockTime = (SELECT DATEDIFF(millisecond, @startTime, GETUTCDATE()))
  SELECT @lockStatus AS Status, @lockTime AS Duration
COMMIT TRANSACTION GetLock

所以我在表上执行SELECT 并使用TABLOCKXHOLDLOCK,所以我在整个表上获得了排他锁并将其保留到事务结束。然后根据结果,我要么返回失败状态(0),要么创建一个新记录并返回(1)。

但是,我不时收到此异常,我只是不知道它是如何发生的:

System.Data.SqlClient.SqlException:违反主键约束“PK_GuidLocks”。无法在对象“dbo.GuidLocks”中插入重复键。重复键值为 (XXXXXXXXX)。该语句已终止。

知道这是怎么发生的吗?两个线程怎么可能在同一张表上获得了排他锁,并试图同时插入行?

更新:看来读者可能还没有完全理解我这里的问题,所以我想详细说明一下:我的理解是使用TABLOCKX获得了一个独占锁桌子。我还从文档中了解到(我可能会弄错),如果我使用 HOLDLOCK 语句,那么锁将一直保持到事务结束,在这种情况下,我假设(显然我的假设是错误的,但是这就是我从文档中了解到的)是由BEGIN TRANSACTION 语句发起并由COMMIT TRANSACTION 语句结束的外部事务。所以我在这里理解的方式是,当 SQL Server 到达具有 TABLOCKX 和 HOLDLOCK 的 SELECT 语句时,它会尝试获取整个表的排他锁,并且在执行COMMIT TRANSACTION 之前不会释放它。如果是这样,两个线程怎么会同时尝试执行相同的 INSERT 语句?

【问题讨论】:

  • 将您的隔离级别设置为 SERIALIZABLE 而不是 READ COMMITTED;这就是对一系列事务强制执行可序列化的方式。
  • @PieterGeerkens,但这是我在 HOLDLOCK 的文档中找到的:“相当于 SERIALIZABLE。有关更多信息,请参阅本主题后面的 SERIALIZABLE。HOLDLOCK 仅适用于表或视图它是指定的,并且仅在使用它的语句定义的事务期间。HOLDLOCK 不能在包含 FOR BROWSE 选项的 SELECT 语句中使用。"
  • 以防万一您不知道它存在,您可能需要查看 sp_getapplock:msdn.microsoft.com/en-us/library/ms189823.aspx

标签: sql-server tsql


【解决方案1】:

如果您在documentation 中查找 tablock 和 holdlock,您会发现它并没有按照您的想法执行:

Tablock:指定在表级别应用获取的锁。这 获取的锁类型取决于正在执行的语句。 例如,一个 SELECT 语句可能会获取一个共享锁。经过 指定TABLOCK,共享锁应用于整个表 而不是在行或页面级别。如果还指定了 HOLDLOCK, 表锁一直保持到事务结束。

因此,您的查询不起作用的原因是因为您仅从表中获取共享锁。 Frisbee 试图指出的是,您不需要重新实现所有事务隔离和锁定代码,因为有一种更自然的语法可以隐式处理。他的版本比你的好,因为它更容易不犯引入错误的错误。

更一般地,在查询中对语句进行排序时,应将需要更严格锁的语句放在最前面。

【讨论】:

  • 你的意思是我的答案,当前分数为 -1
  • 我猜你答案的分数是基于解释的质量,而不是答案的正确性。我试图提供您的答案中缺少的内容,同时感谢您所做的工作。
  • @AlexWeitzer,感谢您的解释。我实际上已经阅读了文档,但是您粘贴的文档是针对 TABLOCK 而不是 TABLOCKX。请注意,在我的查询中,我使用的是 TABLOCKX,因此它应该在表上获得一个排他锁。这是我在文档中找到的:“指定对表进行排他锁。”
  • 我还指定了 HOLDLOCK,如果我理解正确(显然不是),它应该保持锁定直到事务结束,在这种情况下是外部事务。
  • 所以我的理解是,当SQL到达我的SELECT语句时,它会尝试为整个表获取一个排他锁,这个锁只能被一个和一个唯一的。因此,即使我有数千个线程执行相同的查询,也只有一个能够获得该锁,并且在执行语句 COMMIT TRANSACTION 之前不会释放它。
【解决方案2】:

在我多年前的并发编程文本中,我们读到了盲人火车工程师的寓言,他们需要通过一条轨道来双向运输火车,穿过安第斯山脉只有一条轨道宽。在第一个互斥锁模型中,工程师会走到通道顶部的同步碗处,如果它是空的,则在里面放一块小石子锁定通道.开车通过山口后,他会取出鹅卵石以解锁下一班火车的山口。这是您实现的互斥锁模型,它不起作用。在这个比喻中,实施后不久就发生了裂缝,果然碗里有两颗鹅卵石——由于多线程环境,我们遇到了 READ-READ-WRITE-WRTE 异常。

然后这个比喻描述了第二个互斥体模型,其中碗里已经有一个鹅卵石。每个工程师走到碗边,如果有鹅卵石,就把它移走,然后在他开车通过山口时把它放在口袋里。然后他恢复鹅卵石以解锁下一班火车的通行证。如果工程师发现碗是空的,他会继续尝试(或阻塞一段时间)直到有鹅卵石可用。这是可行的模型。

您可以通过在 GuidLocks 表中使用()单行(默认情况下) 锁持有者的 NULL 值。在合适的事务中,如果旧值是 NULL,则每个进程都使用它的 SPID 更新(就地)这一单行;如果成功则返回 1,如果失败则返回 0。它在释放锁时再次将此列更新为 NULL。

这将确保被锁定的资源实际上包括正在修改的行,在您的情况下显然并非总是如此。

请参阅usrthis question 的答案以获取有趣的示例。

我相信您对错误消息感到困惑 - 显然引擎在测试锁是否存在之前定位潜在冲突的行,从而导致误导性错误消息,并且由于(由于实现模型上面的 1 而不是模型 2) TABLOCK 保存在 SELECT 使用的资源上,而不是 INSERT/UPDATE 使用的资源上,第二个进程能够潜入。

请注意,特别是在支持快照隔离的情况下,您在其上获取 TABLOCKX 的资源(任何插入之前的表快照)不保证包含您所使用的资源写了锁定细节(表快照插入之后)。

【讨论】:

  • 请注意,互斥保护子句必须始终采用 write and test 形式而不是 形式。测试和编写
  • 感谢@PieterGeerkens。现在这完全有道理。显然,我和你发布的问题中的“usr”一样困惑。
  • P.S. - 如果您想要可序列化的行为,请请求 SERIALIZABLE 隔离级别,而不是尝试使用特定于实现和特定于版本的等效项来模仿它。 KISS - 保持简单 Smartypants。
【解决方案3】:

使用app lock

exec sp_getapplock @resource = @lockName, 
     @LockMode='Exclusive', 
     @LockOwner = 'Session';

从很多角度来看,您的方法都不正确:粒度(表锁)、范围(提交的事务)、泄漏(将泄漏锁)。会话范围应用锁是您实际打算使用的。

【讨论】:

  • 能否详细说明粒度、范围和泄漏?
  • 另外,如果我的代码失败并且应用程序锁没有释放会发生什么?
  • 会话范围的应用锁在会话(连接)关闭时释放,这将在您的应用程序崩溃时发生。不像表插入和提交的方法,它会泄漏锁。
【解决方案4】:
INSERT INTO GuidLocks 
select @lockName, GETUTCDATE()   
where not exists ( SELECT * 
                   FROM GuidLocks
                   WHERE Id = @lockName );
IF @@ROWCOUNT = 0 ...

注意optimization

SELECT 1 
FROM GuidLocks

【讨论】:

  • 谢谢@Frisbee。这和我的有什么不同?我的意思是我知道你把这一切都放在一个语句中,但我有BEGIN TRANSACTEND TRANSACT,所以HOLDLOCK 应该会导致整个事务都保持锁定,不是吗?
  • 你不认为这里的一个陈述会有所作为吗?
  • 你看我的评论了吗? “我的意思是我知道你把这一切都放在一个语句中,但我有 BEGIN TRANSACT 和 END TRANSACT,所以 HOLDLOCK 应该会导致整个事务都保持锁定,不是吗?”
  • 是的,我阅读了您的问题和评论。 应该 - 它不起作用。你试过我的解决方案了吗?你看我的评论了吗?
  • 嗯,我真的很想了解事情是如何运作的,而不是仅仅从互联网上获取一些东西而不了解它并且以后无法维护它。如果您再次阅读我的问题,我试图了解为什么我的代码不起作用。最后,我不确定“您是否阅读了我的评论”是什么意思,因为我在您的解决方案中看不到任何评论或解释。当我问你时,你添加的唯一评论是“你不认为一个陈述......”,这真的没有回答我第一个评论中的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-24
相关资源
最近更新 更多