为什么 SQL 服务器使用不同的密钥范围锁定 SELECT?

Why does SQL Server lock SELECT with a different key range?

我有两笔交易接二连三地开始:

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]

From what I understand the first transaction should really obtain a key-range lock for the Id = 1, while the other should obtain a key-range lock for id 2.

无论哪种方式都采用键范围锁,范围具有上下边界,索引类型(唯一或非唯一)定义了两个边界的 values/limits:

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

Unique : 下键范围边界 = 上键范围边界 = index/row 值

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

当索引唯一时,第一个查询 selects Id=1 并且这将独占锁定 (XLOCK) 从 index_value = 1 到 index_value=1 的键范围。 第二次查询,对于Id=2,可以select该行,因为Id=2没有加锁。

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

如果 table 有第三行,值为 3..5..10,那么第二个查询 selecting 任何这些值都可以正常工作,因为键-范围锁定是从 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

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

在上面的example/code中,selecting Id=34,不存在,会锁定范围Id= 22 - ∞ (max range of datatype)。尝试 select Id = 25 的第二个查询(在另一个会话中)将被阻止(因为它也需要锁定范围 22 - ∞)。 sys.dm_tran_locks 或 sp_lock 将仅报告键范围的一个值(上限):(ffffffffffff)==∞。下边界(推断?)= max(id).

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

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