
В одной из предыдущих заметок в своём блоге я писал о ряде сюрпризов, к которым нужно быть готовым при использовании "грязного чтения" (уровень изоляции
READ UNCOMMITTED) -
http://www.t-sql.ru/post/nolock.aspx.
А именно:
- "Фантомные" записи, которых нет в БД
- Чтение не всех записей из таблицы
- Чтение одной и той же записи несколько раз
Сегодня я покажу ещё один неприятный момент - это чтение промежуточного состояния изменяемой строки, т.е. при изменении записи в одной атомарной операции:
update MyTable
set a = b
, b = a
В другой транзакции с уровнем изоляции READ UNCOMMITTED можно прочитать запись, когда значение поля
a уже равно
b, а поле
b ещё не изменено.
Пример:
if object_id ( N'dbo.MyTable', N'U' ) is not null
drop table dbo.MyTable;
go
create table dbo.MyTable ( a int, b int );
go
--Вставляем 50 строк
insert into dbo.MyTable ( a, b )
values( 1, 2 );
go 50
--Некластерный индекс, для моделирования ситуации index seek + RID lookup (key lookup)
create index xxx on dbo.MyTable ( a );
go
Теперь в первой транзакции в бесконечном цикле меняем значения полей:
--Бесконечный цикл
while ( 1 = 1 )
begin
update dbo.MyTable
set a = b
, b = a;
end
go
Во второй транзакции, так же в цикле, ищем записи где и
a = 1 и
b = 1. При этом мы используем подсказку
nolock ("грязное чтение") и заставляем оптимизатор задействовать поиск по некластерному индексу
xxx, который мы создали по полю
a.
while ( 1 = 1 )
begin
if exists( select * from dbo.MyTable with ( nolock, index(xxx) )
where a = 1
and b = 1
)
raiserror( 'В таблице есть запись, где a = 1 и b = 1!', 16, 1 ) with nowait;
end
go
Теперь я немного поясню, зачем мы эмулируем ситуацию index seek + RID lookup (key lookup). Т.к. при поиске по индексу
xxx мы обращаемся к страницам индекса, в которых нет данных по полю
b, то оптимизатору необходимо по RID-у (или по кластерному индексу, если бы таблица была кластеризованной) обратиться к страницам с данными, где находится значение поля
b. При этом сиквел накладывает, неуправляемые пользователем, кратковременные блокировки - латчи (
Latch) на эти страницы, а мы, при "грязном чтении" успеваем влезть между этими латчами. В отличии от классических блокировок (
Lock), блокировки
Latch накладываются только на время выполнения физической операции в памяти, а не на время всей логической транзакции.
Графически наш план запроса выглядит следующим образом:
Ну а мы наблюдаем появление всё новых и новых записей:
Не останавливая эти две транзакции, в новом окне попытаемся поймать наши латчи:
select last_wait_type, wait_resource
from sys.dm_exec_requests
where session_id = 57;
Номер моей 2-ого сессии = 57. После нескольких запусков этого запроса я вижу, что в моём случаи, это два ожидания:
last_wait_type | wait_resource |
PAGELATCH_SH | 5:1:163 |
PAGELATCH_SH | 5:1:150 |
BOL: PAGELATCH_SH - Имеет место, когда задача ожидает кратковременной блокировки буфера, находящегося не в состоянии запроса ввода-вывода. Запрос на кратковременную блокировку производится в режиме общего доступа.
Если смотреть дальше, то можно увидеть, что эти кратковременные ожидания в первом случаи страницы индекса, во втором сами данные нашей таблицы:
dbcc traceon(3604);
go
dbcc page ( 5, 1, 163, 3 );
go
dbcc page ( 5, 1, 150, 3 ) with tableresults;
go