为什么 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 上进行锁定)。
我有两笔交易接二连三地开始:
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 上进行锁定)。