在READ COMMITTED隔离中,是否可以在更新后读取旧值?

Ian*_*oyd 4 sql-server-2008-r2

我正在尝试解决 SQL Server 2008 R2 的问题,但我正在抓紧稻草。

鉴于数据位于 B 树的叶节点中,并且也存在于索引中,有人可以在单个UPDATE语句中读取多个值吗?

想象一个进程更新一行:

--Change status from Pending -> Waiting
BEGIN TRANSACTION
   UPDATE Transactions SET Status = 'Waiting'
   WHERE TransactionID = 12345
COMMIT TRANSACTION
Run Code Online (Sandbox Code Playgroud)

是否有可能在此期间BEGIN TRANS; UPDATE; COMMIT另一个进程可以同时读取新旧值?

我想知道这一点,因为更新不会只在一个地方改变一个值。该值将存在于多个地方:

  • 聚集索引的叶节点
  • IX_交易_1
  • IX_交易_2
  • IX_交易_3
  • IX_交易_4
  • IX_交易_5
  • ...
  • IX_Transactions_n

如果更新开始,它必须更新所有这些位置的值。如果另一个进程使用索引执行计划来查找值会发生什么:

  • IX_Clustered待定等待
  • IX_Transactions_1待定等待
  • IX_Transactions_2待定等待
  • IX_Transactions_3 : 待处理
  • IX_Transactions_4 : 待处理
  • IX_Transactions_5:待处理
  • ...
  • IX_Transactions_n : 待处理

在那一刻,如果发出一个查询来查看聚集索引中的值,它将找到Waiting

但是如果一个查询使用IX_Transactions_4它会发现一个值Pending

取消记录锁定机制

内部的锁顺序没有记录,但我假设 SQL Server 对聚集和非聚集索引采用共享锁:

  • IX_Clustered : 共享
  • IX_Transactions_1 : 共享
  • IX_Transactions_2 : 共享
  • IX_Transactions_3 : 共享
  • IX_Transactions_4 : 共享
  • IX_Transactions_5 : 共享

然后将这些锁升级为更新锁:

  • IX_Clustered共享更新
  • IX_Transactions_1共享更新
  • IX_Transactions_2共享更新
  • IX_Transactions_3共享更新
  • IX_Transactions_4共享更新
  • IX_Transactions_5共享更新

此时,另一个进程仍然可以读取这5个索引中的值;因为选择与更新锁兼容。

然后更新继续。如果我们在瞬间拍一张照片:

  • IX_Clustered : Shared Update Exclusive -> 值已更改
  • IX_Transactions_1共享 更新独占 -> 值已更改
  • IX_Transactions_2共享 更新独占
  • IX_Transactions_3共享更新
  • IX_Transactions_4共享更新
  • IX_Transactions_5共享更新

另一个进程不能再从具有排他锁的项目中读取值,但它仍然可以从仍然具有更新锁的项目中读取值(共享锁与更新锁兼容

除非这当然不是发生的事情

这仅在 SQL 仅根据需要升级为独占时才有效。如果相反,它会:

  • IX_Clustered : S U IX X -> 值已更改
  • IX_Transactions_1 : S U IX X -> 值已更改
  • IX_Transactions_2 : S U IX X
  • IX_Transactions_3 : S U IX
  • IX_Transactions_4 : S U IX
  • IX_Transactions_5 : S U IX

我不能再打字了;我继续的意志被粉碎了。就像我说的,我正在抓紧稻草。

Mar*_*ith 6

您可以使用跟踪标志1200打印出锁定信息和3916打印日志信息。(两个无证AFAIK)

对于以下测试设置

CREATE TABLE T
(
A INT IDENTITY PRIMARY KEY,
B INT
)

CREATE NONCLUSTERED INDEX IX ON T(B)

INSERT INTO T 
SELECT number
FROM master..spt_values
Run Code Online (Sandbox Code Playgroud)

然后运行以下两次(因为第二次运行不包含编译期间获取的无关锁)

DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

SET NOCOUNT ON;

UPDATE T 
SET B += 1
WHERE A = 1000

/*If you habitually have 3604 on then change the below*/
DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS
Run Code Online (Sandbox Code Playgroud)

这将返回以下输出

进程 54 在 OBJECT 上获取 IX 锁:11:245575913:0(class bit2000000 ref1)结果:OK

进程 54 在 PAGE 上获取 IU 锁:11:1:127(class bit0 ref1)结果:OK

进程54获取KEY上的U锁:11:72057594039042048 (1f00de11a529) (class bit0 ref1) 结果:OK

进程 54 在 PAGE: 11:1:127 (class bit2000000 ref0) 上获取 IX 锁结果:OK

进程54获取KEY上的X锁:11:72057594039042048 (1f00de11a529) (class bit2000000 ref0) 结果:OK

XdesId 0000:00000423: m_logReserved 9162, delta 9162, op LOP_BEGIN_XACT, caller GenerateLogRec, LSN 00000027:00000159:0001。

XdesId 0000:00000423: m_logReserved 9379, delta 217, op LOP_MODIFY_ROW, caller GenerateLogRec, LSN 00000027:00000159:0002。

进程 54 在 PAGE: 11:1:166 (class bit2000000 ref1) 上获取 IX 锁结果:OK

进程54获取KEY上的X锁:11:72057594039107584 (dbf572a551f4) (class bit2000000 ref1) 结果:OK

XdesId 0000:00000423: m_logReserved 9597, delta 218, op LOP_DELETE_ROWS, caller GenerateLogRec, LSN 00000027:00000159:0003。

进程 54 在 PAGE: 11:1:166 (class bit2000000 ref0) 上获取 IX 锁结果:OK

进程54获取KEY上的RangeI-N锁:11:72057594039107584 (a39688da7d04) (class bit1000000 ref1) 结果:OK

进程54获取KEY上的X锁:11:72057594039107584 (82cbfa4b30c1) (class bit2000000 ref0) 结果:OK

XdesId 0000:00000423: m_logReserved 9671, delta 74, op LOP_INSERT_ROWS, caller GenerateLogRec, LSN 00000027:00000159:0004。

进程 54 在 KEY 上释放锁引用:11:72057594039107584 (dbf572a551f4)

进程 54 在 PAGE 上释放锁定引用:11:1:166

进程 54 在 KEY 上释放锁定引用:11:72057594039042048 (1f00de11a529)

进程 54 在 PAGE 上释放锁定引用:11:1:127

XdesId 0000:00000423: m_logReserved 0, delta -9671, op LOP_COMMIT_XACT, caller GenerateLogRec, LSN 00000027:00000159:0005。

非聚集索引 ( 1:166)上没有锁定,直到在LOP_MODIFY_ROW聚集索引键上更新之后(在页面上1:127),所以是的,这将是可能的。

以上是一个狭窄的(每行)更新计划。

使用广泛的(每个索引)计划,每个索引都会依次更新。可能会进行排序操作,如下面的计划所示。

在此处输入图片说明

这将使机会窗口更宽,但是,虽然事务可能从尚未更新的索引中读取“旧”值,但读提交事务不可能读取该值的“新”版本,直到事务已提交。

但是,读提交语句当然可以读取相同值的两个不同提交版本。

在一个连接运行

WHILE ( 1 = 1 )
  BEGIN
      UPDATE T
      SET    B += 1
      WHERE  A = 1000;
  END 
Run Code Online (Sandbox Code Playgroud)

然后在另一个运行中

SELECT *
FROM   T T1 WITH(READCOMMITTEDLOCK )
       INNER HASH JOIN T T2  WITH(READCOMMITTEDLOCK )
         ON T1.A + 0 = T2.A + 0
WHERE  T1.A = 1000
       AND T2.A = 1000
Run Code Online (Sandbox Code Playgroud)

对我来说,大约 50% 的时间第二个查询返回具有不同B值的行,因为两个查找之间的值发生变化T