我的理解是:
step1,假设表里有100行有序记录, 事务1从row 1 开始读取到了row 50 并准备继续读取完这100行。
要注意的是,sql server 会自动释放已经读取了的row的锁。
step2,这时候,另外一个事务2 修改了 事务1已经读取并且被 sql server 释放掉锁的前面50行中的某些数据。
这修改操作导致了数据排序发生变化,可能原来已经读取了的第20行现在排到了50行后面去。
step3,然后,事务1继续读取剩下的数据。 就可能发现有数据被重复读取出来了。
demo如下:
1. 建立表和填充数据。
CREATE TABLE dbo.testDoubleRead ( id int identity(10,1) PRIMARY KEY, text_asc nvarchar(20) ) go CREATE NONCLUSTERED INDEX IX_testDoubleRead_text_asc ON dbo.testDoubleRead(text_asc ASC); INSERT INTO dbo.testDoubleRead (text_asc) VALUES(‘A‘),(‘B‘),(‘C‘),(‘D‘)
注意非聚集索引的排序是 ASC,这时候表里的数据是:
SELECT * FROM dbo.testDoubleRead /* id text_asc 10 A 11 B 12 C 13 D */
2. 开一个新session,执行下面这段不完整的事务:
--SESSION 1 SCRIPT BEGIN TRANSACTION UPDATE dbo.testDoubleRead SET text_asc = ‘UPDATE_C‘ WHERE id = 12
3. 开另外一个session, 执行下面的查询:
SELECT * FROM dbo.testDoubleRead
显然,这查询是会被session1 block住的。
4. 回到session1, 执行完毕如下代码:
UPDATE dbo.testDoubleRead SET text_asc = ‘UPDATE_B_2‘ WHERE id = 11 COMMIT TRANSACTION
5. 这时候, session2 会查询完毕,结果如下:
SELECT * FROM dbo.testDoubleRead /* id text_asc 10 A 11 B 13 D 11 UPDATE_B_2 12 UPDATE_C */
可以看到,有两条 id = 11的记录!
再次分析下:
1。按照 非聚集索引的定义,刚开始的数据应该是这样的
/* id text_asc 10 A 11 B 12 C 13 D */
2. session1 开启事务并修改了id=12 的行,但没有提交。 这时候,session2 尝试读取数据 但只能读取到 id=11的,不能再朝下读取了;除非session1 释放id=12 的行。
3. 接上面,session1 把id=12 的行修改了,根据非聚集索引的排序规则 text_asc ASC,可以确定值‘UPDATE_C’是应该排到最后一行的。 而且id=11的新值‘UPDATE_B’是在倒数第二行。
理想数据的样子应该是这样的:
/* id text_asc 10 A 13 D 11 UPDATE_B_2 12 UPDATE_C */
4.session修改完id=11以后就提交,这样session2就可以继续之前的查询了。注意的是,session2在被block之前已经读取了: (id=11,text_asc=‘B‘)
5.综合上面的几点,session2继续按照非聚集索引的顺序读取(id=13开始),而不会清空之前已经读取完毕的(id=11,text_asc=‘B‘)。
所以最终的结果就是这样的了:
/* id text_asc 10 A 11 B --在被session1的修改事务block之前读取了这行 13 D --session1提交事务以后,session2从这行继续读 11 UPDATE_B_2 12 UPDATE_C */