主键冲突,即使TABLOCKX和HOLDLOCK提示
我有一个表,用于创建具有唯一键的锁,以控制多个服务器上的关键部分的执行,即一次只有一个线程可以从所有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
并使用TABLOCKX
和HOLDLOCK
这样我就在整个表上获得一个排它锁,并将其保留到事务结束。 然后根据结果,我要么返回失败状态(0),要么创建一个新记录并返回(1)。
然而,我不时得到这个例外,我只是不知道它是如何发生的:
System.Data.SqlClient.SqlException:违反PRIMARY KEY约束'PK_GuidLocks'。 无法在对象'dbo.GuidLocks'中插入重复键。 重复的键值是(XXXXXXXXX)。 该语句已终止。
任何想法如何发生? 两个线程怎么可能在同一个表上获得独占锁并尝试同时插入行?
更新 :看起来读者可能还没有完全理解我的问题,所以我想详细说明一下:我的理解是,使用TABLOCKX会在表上获得排它锁。 我也从文档中理解(我可能会误解),如果我使用HOLDLOCK语句,那么锁将一直持续到事务结束,在这种情况下,我假设(显然我的假设是错误的,但这就是我从文档中理解的)是由BEGIN TRANSACTION
语句启动并以COMMIT TRANSACTION
语句结束的外部事务。 所以我在这里理解的方式是,当SQL Server到达带有TABLOCKX和HOLDLOCK的SELECT语句时,它将尝试在整个表上获得排它锁,并且在执行COMMIT TRANSACTION
之前不会释放它。 如果是这样的话,那么两个线程如何在同一时间尝试执行相同的INSERT语句?
在许多年前我的并发编程文本中,我们阅读了盲人火车工程师的比喻,他们需要通过跨越安第斯山脉的单个道路传输双向传输火车,只有一个道宽。 在第一个互斥模型中,工程师会走到通道顶部的同步碗,如果它是空的,则放入一颗卵石来锁定通道。 在通过通行证后,他将取下他的卵石,解开下一班列车的通行证。 这是你已经实现的互斥模型,它不起作用。 在这个寓言中,一个crach在实现后不久就发生了,当然,碗中有两个鹅卵石 - 由于多线程环境,我们遇到了读 - 写 - 写的异常。
这个寓言随后描述了第二个互斥模型,在这个模型中,碗里已经有一颗卵石。 每个工程师走到碗里,取出卵石,如果有人在那里,把它放在口袋里,而他通过通行证。 然后他恢复卵石解开下一班火车的通行证。 如果一名工程师发现碗空了,他会一直尝试(或阻塞一段时间),直到有一块卵石。 这是有效的模型。
您可以通过在GuidLocks表中只有一行(默认情况下)为锁持有者设置NULL值来实现此(正确)模型。 在一个合适的事务中,每个进程更新(就地)这个单一的行与它的SPID,如果旧的值是NULL; 如果成功则返回1,如果失败则返回0。 当它释放锁时,它再次将此列更新回NULL。
这将确保被锁定的资源实际上包含被修改的行,这在你的情况下显然不总是正确的。
请参阅usr对这个问题的答案,以获得一个有趣的例子。
我相信你对错误信息感到困惑 - 显然引擎在测试锁定的存在之前定位了潜在冲突的行,导致错误的错误消息,并且由于(由于实现了上面的模型1模型2)TABLOCK被SELECT所使用的资源而不是INSERT / UPDATE所使用的资源所持有,第二个进程可以潜入。
请注意,特别是在支持快照隔离的情况下,您已将TABLOCKX(在任何插入之前的表快照)的资源不保证包含已写入锁定细节的资源(表快照之后插入物)。
如果你查看tablock和holdlock的文档,你会发现它并没有按照你的想法进行:
Tablock:指定获取的锁在表级应用。 获取的锁的类型取决于正在执行的语句。 例如,一条SELECT语句可能会获取一个共享锁。 通过指定TABLOCK,共享锁应用于整个表格,而不是在行或页面级别。 如果还指定HOLDLOCK,则表锁将一直保留到事务结束。
所以你的查询不起作用的原因是你只能从表中获得一个共享锁。 什么飞盘试图指出的是,你不需要重新实现所有的事务隔离和锁定代码,因为有一种更自然的语法来隐式地处理这个事件。 他的版本比你的版本更好,因为不会犯错引入错误更容易。
更一般地说,在查询中排序语句时,应首先放置需要更严格锁定的语句。
使用应用锁。
exec sp_getapplock @resource = @lockName,
@LockMode='Exclusive',
@LockOwner = 'Session';
从许多角度来看,你的方法是不正确的:粒度(表锁),作用域(事务提交),泄漏(将泄漏锁)。 会话范围应用锁定是您实际打算使用的。
链接地址: http://www.djcxy.com/p/5621.html上一篇: Primary key conflict even when TABLOCKX and HOLDLOCK hints