【问题标题】:Why does SQL Server lock SELECT with a different key range?为什么 SQL Server 使用不同的键范围锁定 SELECT?
【发布时间】:2023-03-04 20:59:01
【问题描述】:

我有两个事务一个接一个地开始:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRANSACTION
select * from MyTable WITH (XLOCK) WHERE Id = 1
WAITFOR DELAY '00:00:10';
COMMIT TRANSACTION

第二个几乎一样(只是没有延迟和另一个id)

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRANSACTION
select * from MyTable WITH (XLOCK) WHERE Id = 2
COMMIT TRANSACTION

我在 ID 上有一个非唯一索引。当至少在 Id 上有一个主键时,这似乎确实可以按预期工作。

据我了解,第一个事务应该真正获得 Id = 1 的键范围锁,而另一个应该获得 id 2 的键范围锁。

显然它不会像那样工作,因为第二个事务一直卡在第一个事务完成之前。我是否遗漏了某些内容,或者我无法强制独占键范围锁定?

这是我的示例的完整创建脚本:

CREATE TABLE [dbo].[MyTable](
    [Id] [bigint] NOT NULL,
 CONSTRAINT [PK_MyTable] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

【问题讨论】:

  • Id 是集群主键吗?什么 SQL Server 版本?
  • 最后一个 Sql 服务器版本(Azure 数据库),它是一个聚集的主键,但我希望它与一个更复杂的非聚集索引一起工作,包括 2 列(像id = 1 and time > now 这样的钥匙锁)
  • 我无法使用 Id 上的主键进行复制,无论是集群还是非集群。您可以验证 Id 上的索引是否存在?不一定是PK,只要是唯一的即可。
  • 我的索引不会是唯一的,它必须是吗?我可以从示例中仔细检查主索引
  • 我已经仔细检查了:Id 是表上的主键,将示例从 SO 复制到 Management Studio 中并且每次都可以重现,也许还有其他东西会影响这个?也许我拥有的其他领域会影响它?

标签: sql sql-server transactions locking


【解决方案1】:

据我了解,第一笔交易应该真正获得 Id = 1 的 key-range lock,而另一个应该获得一个 id 2 的键范围锁定。

键范围锁定采用任一方式,范围具有上下边界,索引类型(唯一或非唯一)定义了两个边界的值/限制:

假设,对现有索引值的查询(如问题中,id=1 & id=2), 当索引为:

唯一:下键范围边界 = 上键范围边界 = 索引/行值

非唯一:范围下限 = 索引/行值 & 范围上限 = 索引的下一个值(如果有)。

当索引唯一时,第一个查询选择 Id=1,这会以独占方式锁定 (XLOCK) 从 index_value = 1 到 index_value=1 的键范围。 第二个查询,对于 Id=2,可以选择行,因为 Id=2 没有被锁定。

当索引不唯一时,第一个查询(Id=1)独占锁定从 index_value = 1 到 index_value=2 的键范围。 第二个查询(对于 Id=2)无法选择行,因为 Id=2 已被第一个查询(独占)锁定。

如果表有第三行,值为 3..5..10,那么选择这些值中的任何一个的第二个查询都可以正常工作,因为键范围锁是从 Id 1 到 2。

create table mytable(Id bigint not null, index idxId /*unique*/ nonclustered (id));
go
insert into mytable(Id) values (1), (2), (5), (7), (20), (21), (22);
go


set transaction isolation level serializable;
begin transaction
select * from mytable with(xlock) where id = 1;

--nonunique index: rangeXX, 
--........locked values: 1&2 for select...Id=1
--........locked values: 2&5 for select...Id=2
select tl.request_mode, tl.request_type, tl.request_status, tl.resource_description, irs.*
from
sys.dm_tran_locks as tl 
left join
(
select %%lockres%% as idxresourcedescription, Id as [column:Id/value]
from mytable with (index(idxId), nolock)
) as irs
on tl.resource_description = irs.idxresourcedescription;

--rollback transaction
go


--in another session/window
select * from mytable with(xlock, serializable) where id = 5; --this is not blocked...
raiserror('', 0, 0) with nowait;
select * from mytable with(xlock, serializable) where id = 2; --...but this is blocked
go

在尝试选择(独占和可序列化)一个不存在的值时,评估范围锁会变得稍微“复杂”一些(正如您在评论中注意到的那样,关于 (ffffffffffff) 范围锁)。

在上面的示例/代码中,选择不存在的 Id=34 将锁定范围 Id= 22 - ∞(数据类型的最大范围)。尝试选择 Id = 25 的第二个查询(在另一个会话中)将被阻止(因为它也需要锁定范围 22 - ∞)。 sys.dm_tran_locks 或 sp_lock 将只报告 keyrange 的一个值(上限): (ffffffffffff)==∞。下界(推断?)= max(id)。

因此,选择 Id=-20 将锁定键范围 [-∞] - 1 并且 sys.dm_tran_locks 报告一个边界 (Id=1)。下边界(推断)= [-∞]。

当表具有 ID (7)、(20) 并且查询选择 Id=15(具有非唯一和/或唯一索引)时,您可以尝试猜测键范围锁。从概念上讲,在可序列化事务中有一个键范围锁,并且需要对现有的键/值进行锁定(没有 Id=15 的行,锁必须在不同的键/值上)。

【讨论】:

  • 感谢您提供非常好的和全面的答案!这确实阐明了它是如何工作的。它总是阻止需要的东西,但在不同的情况下可以阻止更多的东西作为附带损害。实际上,这在我的场景中是可以接受的,但我会尝试使用 sp_getapplock 来阻止我自己需要的资源(我有一个可以在每个操作中使用的逻辑 ID)。然后我不会为 SELECT 使用排他锁,而是获取应用程序锁。与可序列化事务级别 + 排他锁相比,您知道我是否可以预料到一些麻烦?
猜你喜欢
  • 2013-12-26
  • 2015-10-07
  • 1970-01-01
  • 1970-01-01
  • 2011-01-27
  • 1970-01-01
  • 2012-08-03
  • 1970-01-01
  • 2011-02-27
相关资源
最近更新 更多