即使TABLOCKX和HOLDLOCK提示

时间:2016-01-26 09:22:07

标签: sql-server tsql

我有一个表,用于创建具有唯一键的锁,以控制多个服务器上关键部分的执行,即一次只有一个线程可以从所有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:违反PRIMARY KEY约束' PK_GuidLocks'。无法在对象' dbo.GuidLocks'中插入重复键。重复键值为(XXXXXXXXX)。声明已经终止。

知道这是怎么回事吗?两个线程如何设法在同一个表上获得独占锁并尝试同时插入行?

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

4 个答案:

答案 0 :(得分:2)

如果你在documentation查找tablock和holdlock,你会发现它没有按照你的想法行事:

  

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

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

更一般地说,在查询中对语句进行排序时,应首先放置需要更严格锁定的语句。

答案 1 :(得分:1)

在我多年前的同步编程文本中,我们读到了盲人列车工程师的比喻,他们需要通过安第斯山脉的单轨道通过两个方向运输火车,只有一个轨道宽。在第一个互斥模型中,工程师会走到通道顶部的同步碗中,如果它是空的,则将一个鹅卵石放入 lock 该通行证。在通过通行证后,他将移除他的鹅卵石,以解锁下一班火车的通行证。这是您实现的互斥模型,但它不起作用。在比喻中,在实施后不久就发生了一个裂缝,果然在碗里有两个鹅卵石 - 由于多线程环境,我们遇到了READ-READ-WRITE-WRTE异常。

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

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

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

请参阅usrthis question的答案,了解一个有趣的例子。

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

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

答案 2 :(得分:-1)

使用app lock

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

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

答案 3 :(得分:-1)

button.layer.cornerRadius =  0.5*button.bounds.size.width

安全optimization

INSERT INTO GuidLocks 
select @lockName, GETUTCDATE()   
where not exists ( SELECT * 
                   FROM GuidLocks
                   WHERE Id = @lockName );
IF @@ROWCOUNT = 0 ...