Грязное чтение и несогласованные данные

by Alexey Knyazev 14. января 2012 23:48
В одной из предыдущих заметок в своём блоге я писал о ряде сюрпризов, к которым нужно быть готовым при использовании "грязного чтения" (уровень изоляции 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_typewait_resource
PAGELATCH_SH5: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

Tags: , , , ,

SQL Server

Добавить комментарий

  Country flag

biuquote
  • Комментарий
  • Предпросмотр
Loading