Tri*_*nko -4 sql-server deadlock concurrency locking sql-server-2019
我想为 M 个用户自动保留 N 个对象的库存。
我有两张库存跟踪表,一张用于全球库存,一张用于个人库存。
有两个表,因为我必须为每个对象以及每个用户强制执行最大允许保留。例如,一个对象可能被限制为总共保留 1000 个,每个用户最多保留 10 个该对象。
全局清单表以 [ObjectID] 为唯一键,个人清单表以 [UserID, ObjectID] 为唯一键。
为 3 个对象保留库存的示例请求如下所示。一些对象有限制,而另一些则没有。
[
{ObjectID: 'A', QuantityToReserve: 1},
{ObjectID: 'C', QuantityToReserve: 2, GlobalMax: 1000, PersonalMax: 10},
{ObjectID: 'B', QuantityToReserve: 5}
]
Run Code Online (Sandbox Code Playgroud)
原子预留是通过启动一个事务,然后首先更新全局清单表中的行来实现的,对于所有对象 ID,按升序排列。
没有限制的对象的更新语句如下所示:
UPDATE [GlobalInventory] WITH (ROWLOCK, XLOCK, HOLDLOCK)
SET [Count] = [Count] + @QuantityToReserve
WHERE [ObjectID] = @ObjectID
Run Code Online (Sandbox Code Playgroud)
具有限制的对象的更新语句如下所示:
UPDATE [GlobalInventory] WITH (ROWLOCK, XLOCK, HOLDLOCK)
SET [Count] = [Count] + @QuantityToReserve
WHERE [ObjectID] = @ObjectID AND ([Count] + QuantityToReserve) < @GlobalMax
Run Code Online (Sandbox Code Playgroud)
类似的更新语句稍后用于更新和锁定个人清单表中的行,并在谓词中添加 [UserID](特别是与 UserIDs 表的连接)。
按顺序(按对象 ID)更新行可以有效地按该顺序在记录上取消排他行锁,然后在事务的其余部分保留这些行(我相信这种情况会发生,即使没有更新语句的 HOLDLOCK 提示)。其他并发事务将阻塞等待事务提交更新的行,然后其他事务才能获得更新这些行所需的锁。因为行锁是按升序取出的,所以一旦获得了集合中的所有锁,就可以保证没有其他事务持有任何这些排他锁。这是公认的事实。锁定顺序很重要(解锁顺序不重要)。问问 Linus:https : //yarchive.net/comp/linux/lock_ordering.html
我的第一个问题是,这些更新语句会按预期工作吗?这个问题有多个部分,例如谓词是否会标识要更新的行,以及该谓词在排他行锁被持有时(即在行更新之前)是否成立?我有正确的锁定提示吗?
由于我们已经确定没有其他事务持有当前持有的任何锁,因此从逻辑上讲,没有其他事务会尝试更新或锁定个人清单表中具有相同对象 ID 的任何记录。
在这一点上,我想我需要强制数据库引擎在更新个人库存行时也使用行级锁。原因是,如果它升级为页面锁定,它可能会无意中锁定恰好在该页面上但不属于事务正在使用的对象 ID 集的记录。
例如,假设一个并发事务锁定了全局记录“D”,因此它似乎与处理对象 A、B 和 C 的记录的第一个事务完全无关。全局或个人库存记录都不应该重叠,所以应该有每个人都在使用的个人库存记录中没有锁争用。但是,如果这个并发事务在个人清单表中取出页级锁,而 D 的某些页恰好包含对象 B 的记录,事务 D 可能会无意中持有属于 D 和 B 的记录的页锁。同样,第一个事务也可能持有包含 B 和 D 的一些记录的页级锁,并且两个事务都不能继续进行,因为每个事务都锁定了另一个正在等待的页。换句话说,
更新个人清单表中的记录有点复杂,因为它必须更新多行。更新语句仍将一次针对一个对象 ID 运行,但它将与建立用户 ID 集的临时表连接。
UPDATE pi WITH (ROWLOCK, XLOCK, HOLDLOCK)
SET [Count] = [Count] + @QuantityToReserve
FROM PersonalInventory pi
INNER JOIN @UserIDs uids on pi.UserID = uids.UserID
WHERE [ObjectID] = @ObjectID AND ([Count] + QuantityToReserve) < @PersonalMax
Run Code Online (Sandbox Code Playgroud)
我的第二个问题是,这个连接到 UserIDs 表的更新语句是否会采用正确的排他行锁,只有由于满足谓词而实际更新的记录?
这种情况对于该系统的正确运行至关重要,所以如果不是,我想知道,我想知道原因。如果未持有预期的锁,则持有哪些锁。请假设我在表上禁用了锁升级。
我担心是否可以强制使用这种行级锁,但后来我发现有一个表选项可以禁用锁升级。我更关心正确性而不是性能。使用数据库行锁将比涉及到服务器的多次往返的任何其他锁定解决方案快许多数量级。使用sp_getapplock
也行不通,因为它会冗余地执行全局清单表上的锁实现的相同功能,同时不采取任何措施来防止我刚刚提到的页面锁蔓延。通过使用数据库、行级锁,多个并发事务可以快速、同时完成,锁争用最少。这将导致原子性的、高吞吐量的库存预留,而不必担心在应用程序级别管理事务,这最终会更复杂且更不可靠。
如果有一种方法可以强制组合键的一部分驻留在不同的数据页上,则页面锁定是可以接受的,但我认为这是不可能的。例如,如果我在 {ObjectID, UserID} 上键入个人表,我必须确保每个页面最多包含一个对象 ID(和许多用户)的记录。
Pau*_*ite 12
我的第一个问题是,这些更新语句会按预期工作吗?
很有可能,但不确定。
SQL Server 保证它将遵守查询的语义,以及由有效隔离级别确定的 ACID 合规级别。除此之外,一切都是实现细节(包括采取什么类型的锁、何时以及持有它们多长时间)。
优化器很可能会为给定的语句和模式描述选择一个单例搜索平凡计划。在执行期间,存储引擎很可能会获得与预期非常相似的锁,但不能保证。
谓词是否会标识要更新的行
这是语句的语义,所以是的。
到排他行锁被持有时(即在行被更新之前),谓词是否成立?
获取排他锁的确切时间取决于实现细节,并且会受到提示和计划形状的影响。SQL Server 确实保证,一旦某个特定行被确定为符合更新条件,它就不会被其他并发事务修改,直到当前事务完成。两个示例更新语句都标识单行,因此不会出现有关面对并发更新活动时的集合成员资格和竞争条件的问题。
我有正确的锁定提示吗?
ROWLOCK
:这个提示(不是“指令”),结合禁用表的锁升级,应该在我能想到的所有情况下产生行级锁定。请注意,锁永远不会升级到页面级别。如果没有这个提示,SQL Server 几乎肯定会为给定的语句和架构选择行锁定。
XLOCK
:SQL Server 通常在标识要更新的行的访问方法上使用更新锁,在修改之前转换为独占(如有必要)。获取更新锁(而不是共享锁)是一种防御转换死锁的常见原因。更新锁与共享锁兼容,但与其他更新锁不兼容。对于单操作员更新,引擎通常会立即获取排他锁。该XLOCK
提示似乎并没有在这种情况下执行一个有用的功能,但可能是无害的。
HOLDLOCK
:这是SERIALIZABLE
隔离级别表提示的同义词。它不会影响锁定本身的持有时间。具有此提示的对象将使用可序列化的隔离语义进行访问,而不管当前有效的隔离级别如何。这对物理锁定的影响可能很复杂,尤其是与其他与隔离相关的提示(如XLOCK
. 例如,更新可能会在没有XLOCK
提示的情况下使用 Range SU ,而使用 Range XX。通常,与共享锁相比,独占锁定感兴趣的键下方的半开范围可能会降低并发性。顺便说一句:尝试为不存在的库存保留库存ObjectID
似乎是一个奇怪的操作,但是可序列化隔离无论如何都会使用 Range XX 锁保护周围的范围。
在我看来,额外的提示似乎没有什么理由。运行不带可序列化事务的简单更新似乎提供了尽可能多的保证。在相同的访问方法上以一致的顺序访问每一行应该提供足够的保护,防止全局清单表上的死锁。
我的第二个问题是,这个连接到 UserIDs 表的更新语句是否会采用正确的排他行锁,只有由于满足谓词而实际更新的记录?
这不太确定,也很难完全评估。优化器可以对该语句的执行计划做出真正的选择。例如,它可以在散列、合并或嵌套循环连接、扫描或查找之间进行选择,以及决定是否调用并行性、通过位图过滤的半连接减少、嵌套循环预取和批处理排序......在许多可能性中。并非所有内部细节都反映在最终用户可见的执行计划表示中。
特别要注意的是,即使优化器选择了问题文本中隐含的迭代执行计划,它也可以选择任一表作为驱动程序。也许“问题”最明显的原因是优化器选择完全扫描个人库存表,并寻找表变量。
这将有可能约束进一步还使用更多的提示(例如优化LOOP JOIN
,FORCE ORDER
,FORCESEEK
等),但不是每一种可能性可以覆盖这个样子。我会选择另一种方法。
由于我们已经确定没有其他事务持有当前持有的任何锁,因此从逻辑上讲,没有其他事务会尝试更新或锁定个人清单表中具有相同对象 ID 的任何记录。
你应该知道这个:
没有其他事务持有当前持有的任何锁
技术上不成立。
在某些情况下,可以为一个值的单个更新获取多个行锁。
即使每个键/行值都是唯一的。
出于开销原因,SQL Server 散列要锁定的行。可以使用未记录的%%lockres%%
函数公开每行的 6 字节哈希值。
根据行数、主键的结构、数据分布和散列算法的复杂性,可能会发生散列冲突。例如,一个计算出的锁哈希值可以锁定一个 B 树中的多行
散列冲突是可能的。什么时候?这取决于™。
更多关于%%lockres%%
碰撞概率的信息可以在Remus Rusanu 的一篇文章中找到。
这篇优秀文章的一个重要部分:
所以 SQL %%lockres%% 散列将产生两个具有相同散列的记录,有 50% 的概率,从表中,任何表,只有 16,777,215 条记录
换句话说,当 SQL Server 识别出要锁定的行时,它不会锁定物理行,而是将与该行对应的哈希值输入到内部锁表中。因为其他行可能具有相同的散列值,所以那些其他不相关的行被有效地锁定,因为它们共享相同的锁散列,因此看起来被另一个事务锁定。
说明这些哈希冲突的示例:
SET NOCOUNT ON;
CREATE TABLE #HashCollision(ID UNIQUEIDENTIFIER PRIMARY KEY NOT NULL);
DECLARE @i int = 1
WHILE @i <= 33
BEGIN
INSERT INTO #HashCollision WITH(TABLOCK)
(ID)
SELECT TOP(1000000) 1M
NEWID()
FROM master..spt_values spt1
CROSS APPLY master..spt_values spt2
SET @i+=1;
END
--33m rows
SELECT %%lockres%% as lockress
INTO #temp
FROM #HashCollision;
select COUNT(DISTINCT lockress)
from #temp;
--when I tested, the count was 32999999.
--Looks like it fluctuates between 32999999 and 32999998 on additional tests
SELECT lockress FROM
#temp
group by lockress
having COUNT(*) > 1;
Run Code Online (Sandbox Code Playgroud)
当我运行这个例子时,我有一个重复的lockres
值:(45642dc72eae)
具有相同%%lockres%%
哈希值的 uniqueidentifier 值是
51300BD6-EE42-435F-92D3-A23AB965C6D6
& A9FEFC2E-3BA9-4C56-8F32-A69811C95092
在第二次检查中,我将值插入到实际表中,找到了另一个lockres
值,(a006f9bf8d84)
并尝试在两个查询窗口中使用返回的两个哈希值更新此表:
交易 1
BEGIN TRAN
UPDATE dbo.HashCollision
SET ID = NEWID()
WHERE ID = 'D616D1C1-D609-448F-A1F4-5F959AB344F3';
Run Code Online (Sandbox Code Playgroud)
交易 2(被阻止)
BEGIN TRAN
UPDATE dbo.HashCollision
SET ID = NEWID()
WHERE ID = '0B6846D9-3E47-4B75-9F7B-89CA0B090524';
Run Code Online (Sandbox Code Playgroud)
直到事务 #1 被回滚或提交。
归档时间: |
|
查看次数: |
285 次 |
最近记录: |