在 SQL Server 中使用行锁更新

update with rowlock in MSSQL server

我试图了解 SQL 服务器中的 ROWLOCK 在锁定记录后更新记录。这是我的观察,我想确认 ROWLOCK 是否像 table 或页面锁之类的东西,或者我没有正确尝试过。 ROWLOCK 应该是只锁定行而不是 table 或页面。

这是我尝试过的:

我创建了一个简单的 table:row_lock_temp_test 有两列 IDName 没有 PK 或者指数。现在我打开 SQL 服务器,两个不同的客户端但凭据相同,并尝试执行一组查询如下:

客户 1:

1: BEGIN TRANSACTION;
2: update row_lock_temp_test set name = 'CC' where id = 2
3: COMMIT

客户 2:

1: BEGIN TRANSACTION;
2: update row_lock_temp_test set name= 'CC' where id = 2
3: COMMIT

我在 C-1 上执行了查询 1、2,然后转到 C-2 并执行了相同的查询,两个客户端都执行了查询,然后我提交了事务,一切都很好。

然后我添加了 RowLock 来更新查询,

C-1

  1: BEGIN TRANSACTION;
  2: update row_lock_temp_test WITH(rowlock) set name = 'CC' where id = 2
  3: COMMIT

C-2

1: BEGIN TRANSACTION;
2: update row_lock_temp_test WITH(rowlock) set name = 'CC' where id = 2
3: COMMIT

现在,我在 C-1 上执行了查询 1 和 2,然后转到 C-2 并尝试执行相同的 2 个查询,但是查询按预期卡住了,因为该行被 C-1 锁定,所以它应该在队列中,直到事务在 C-1 上提交。一旦我提交了 C-1 上的事务,C-2 上的查询就被执行了,然后我也提交了 C-2 上的事务。一切顺利。

在这里我尝试了另一种情况来执行同一组查询,行 id = 3

C-2

 1: BEGIN TRANSACTION;
 2: update row_lock_temp_test WITH(rowlock) set name = 'CC' where id = 3
 3: COMMIT

我在 C-1 中执行了第一个两个查询,然后去执行了 C-2 的第一个两个查询,两个客户端中的行 ID 不同,但 C-2 中的查询仍然卡住了。这意味着在使用 id = 2 更新查询时它锁定了页面或 table,我期待行锁,但它似乎是页面或 table 锁。

我也尝试过使用不同组合的 xlock、HOLDLOCK 和 UPDLOCK,但它总是锁定 table。有没有可能只锁定一行。

Select 并且插入按预期工作。

提前致谢。

锁定提示只是提示。您不能“强制”SQL 获取特定类型的锁。

您可以通过以下查询查看正在获取的锁:

select      tl.request_session_id,
            tl.resource_type,
            tl.request_mode,
            tl.resource_description,
            tl.request_status
from        sys.dm_tran_locks   tl
join        sys.partitions      pt  on  pt.hobt_id = tl.resource_associated_entity_id
join        sys.objects         ob  on  ob.object_id = pt.object_id
where       tl.resource_database_id = db_id()
order by    tl.request_session_id

好的,让我们 运行 SSMS 查询中的一些代码 window:

create table t(i int, j int);
insert t values (1, 1), (2, 2);

begin tran;
update t with(rowlock) set j = 2 where i = 1;

打开第二个 SSMS window,运行 这个:

begin tran;
update t with(rowlock) set j = 2 where i = 2;

第二次执行会被阻塞。为什么?

运行 第三个 window 中的锁定查询,请注意有两行 resource_typeRID,其中一行 status 的“授予”,另一个 status 的“等待”。我们将在一秒钟内到达 RID 位。此外,查看这些行的 resource_description 列。是一样的值。

好的,那什么是 resource_description?这取决于resource_type。但是对于我们的 RID 它表示:文件 ID,然后是页面 ID,然后是行 ID(也称为插槽)。但为什么两次执行都锁定行槽 0?他们不应该试图锁定不同的行吗?毕竟,我们更新的是不同的行。

David Browne 给出了答案:为了找到要更新的正确行,SQL 必须扫描整个 table,因为没有索引告诉它有多少行其中 i = 1。它会在扫描时对每一行进行更新锁定。为什么要对每一行进行更新锁定?好吧,可以这么说,这不是“做”更新。为此需要一个独占锁。几乎总是采用更新锁来防止死锁。

因此,第一个查询扫描了所有行,对每一行进行了 U 锁定。当然,它会立即在插槽 0 中找到它想要更新的行,并获取 X 锁。它仍然有 X 锁,因为我们还没有提交。

然后我们开始第二个查询,它也必须扫描所有行以找到它想要的行。它开始尝试获取第一行的 U 锁,但被阻止了。我们第一个查询的 X 锁阻塞了它。

所以,你看,即使使用行锁定,你的第二个查询仍然被阻止。

好的,让我们回滚查询,看看如果第一个查询更新第二行,第二个查询更新第一行会发生什么?那样有用吗?没有!因为 SQL 仍然无法知道有多少行与谓词匹配。所以第一个查询在插槽 0 上获取更新锁,发现它不必更新它,在插槽 1 上获取更新锁,看到 i 的正确值,获取排他锁,然后等待我们承诺。

查询 2 出现,获取槽 0 上的更新锁,看到它想要的值,获取它的独占锁,更新值,然后尝试获取槽 1 上的更新锁,因为那也可能有它想要的值。

您还会在下一个“级别”(即页面)上看到“意图锁定”。该操作让引擎的其余部分知道它可能希望在将来的某个时候将锁升级到页面级别。但这不是这里的一个因素。页面锁定不是导致问题的原因。

这种情况下的解决方案?在列 i 上添加索引。在这种情况下,这可能是主键。然后您可以按任一顺序进行更新。在这种情况下请求行锁定没有区别,因为 SQL 不知道有多少行与谓词匹配。但是即使你在某些情况下试图force一个行锁,并且即使有一个主键或适当的索引,SQL仍然可以选择升级锁类型,因为它锁定整个页面或整个 table 比锁定和解锁单个行更有效。