Одна из тем, о которых я люблю рассказывать, — журнал транзакций и механизмы журналирования/восстановления. Я читал доклад на эту тему и на PASS, и на SQL Connections, и в обоих случаях обещал опубликовать серию статей о глубинных внутренних аспектах операций журналирования. Перед вами первая из них. Другие мои статьи, посвящённые некоторым деталям журналирования:
- How do checkpoints work and what gets logged (там также объясняется, как стартует восстановление после сбоя: используя загрузочную страницу и самую свежую журнальную запись контрольной точки)
- Finding out who dropped a table using the transaction log
- How expensive are page splits in terms of transaction log?
- Поговорим ещё о циклической природе журнала транзакций
- Ghost Cleanup возвращается
- Подробно об очистке фантомных строк
А теперь — к делу.
В SQL Server в редакции Enterprise существует возможность, называемая быстрое восстановление (fast recovery). Она позволяет базе данных стать доступной для работы сразу после завершения первой фазы восстановления (REDO), ещё до окончания второй (обычно более долгой) фазы (UNDO). Если не понимаете, о чём речь, загляните в мою статью в TechNet Magazine — Ведение журнала и восстановление в SQL Server. Но как SQL Server добивается этого?
Ответ — протоколирование блокировок (lock logging). Каждая журнальная запись описывает одно изменение, внесённое в базу. Для тех записей, которые могут быть использованы в UNDO (да, есть изменения, которые «в одну сторону» — например, изменения страниц PFS), начиная с 2005 года журнал дополнительно включает описание того, какие блокировки удерживались в момент внесения изменения. Эти блокировки были необходимы, чтобы защитить само изменение, когда исходная транзакция выполнялась (до сбоя), а значит, те же самые блокировки будут нужны для защиты анти‑операции, которая это изменение отменяет. О самих анти‑операциях подробнее расскажу в одной из следующих статей из цикла о журналировании.
Движок хранения (Storage Engine) делает три прохода по журналу в рамках восстановления после сбоя. Первый проход определяет, какие транзакции были зафиксированы, и инициирует упреждающее чтение (readahead) нужных страниц файлов данных в буферный пул. Второй проход выполняет REDO, а также читает те журнальные записи, которые будут обрабатываться на третьем проходе (UNDO), — разбирает описания блокировок и фактически захватывает эти блокировки. Для быстрого восстановления на этом этапе база поднимается онлайн. Это возможно потому, что система восстановления знает, что уже удерживает корректные блокировки, гарантирующие, что она безопасно сгенерирует и выполнит анти‑операции, необходимые для UNDO. Побочный эффект: хотя база уже доступна, запрос может упереться в одну из удерживаемых ради быстрого восстановления блокировок — и тогда ему придётся ждать, пока эта блокировка будет снята по мере продвижения UNDO.
Здорово, правда?
Это было введение, чтобы перейти к небольшой «спелеологии» по внутренностям :-) Я создам несколько коротких примеров, чтобы показать протоколирование блокировок в журнале. Не путайтесь: в журнал попадёт не «сама блокировка» (не память, занятая под неё), а лишь описание того, какие блокировки удерживались и в каких режимах.
Ниже скрипт для создания базы данных с простой таблицей. Я использую LOB‑столбец и специально настраиваю хранение вне строки (см. Importance of choosing the right LOB storage technique), чтобы можно было увидеть и блокировки текстовых страниц. Модель восстановления ставлю SIMPLE ради простоты (ха‑ха) — тогда можно очищать журнал контрольной точкой, не возясь с бэкапами лога. Вставлю первую строку и затем очищу журнал.
CREATE DATABASE LockLogging;
GO
USE LockLogging;
GO
CREATE TABLE LockLogTest (c1 INT, c2 INT, c3 VARCHAR (MAX));
GO
EXEC sp_tableoption 'LockLogtest', 'large value types out of row', 'on';
GO
INSERT INTO LockLogTest VALUES (1, 1, 'a');
GO
ALTER DATABASE LockLogging SET RECOVERY SIMPLE;
GO
CHECKPOINT;
GO
Теперь первый опыт — простая вставка — и посмотрим журнальные записи через fn_dblog (пропускаю записи контрольной точки):
INSERT INTO LockLogTest VALUES (2, 2, 'b');
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation Context Page ID Slot ID Locks Lock Information
---------------- ------------- -------------- -------- ----- -------------------------------------------------------------------------------------------------------
LOP_BEGIN_XACT LCX_NULL NULL NULL NULL NULL
LOP_INSERT_ROWS LCX_TEXT_MIX 0001:00000098 1 2 ACQUIRE_LOCK_IX PAGE: 18:1:152; ACQUIRE_LOCK_X RID: 18:1:152:1
LOP_INSERT_ROWS LCX_HEAP 0001:0000009a 1 3 ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:1
LOP_COMMIT_XACT LCX_NULL NULL NULL NULL NULL
Видим IX‑блокировку страницы и X‑блокировку строки для LOB‑значения, вставляемого на текстовую страницу, а также IX‑блокировку таблицы, IX‑блокировку страницы и X‑блокировку строки для записи данных, вставляемой в кучу. Расшифровка ресурсов блокировок:
- 18:1:152 — страница 152 в файле 1 базы с ID 18;
- 18:1:152:1 — слот 1 на странице 152 в файле 1 базы ID 18;
- 18:2073058421:0 — объект с ID 2073058421 (ID таблицы LockLogTest) в базе ID 18.
Обратите внимание и на LOP_BEGIN_XACT, и на LOP_COMMIT_XACT: хотя я не начинал явной транзакции, SQL Server обязан стартовать её не явно, чтобы была граница для отката на случай сбоя внутри операции.
Далее — обновление (с контрольной точкой перед ним, чтобы очистить журнал):
CHECKPOINT;
GO
UPDATE LockLogTest SET c1 = 3;
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation Context Page ID Slot ID Locks Lock Information
---------------- ------------- -------------- -------- ----- -------------------------------------------------------------------------------------------------------
LOP_BEGIN_XACT LCX_NULL NULL NULL NULL NULL
LOP_MODIFY_ROW LCX_HEAP 0001:0000009a 0 3 ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:0
LOP_MODIFY_ROW LCX_HEAP 0001:0000009a 1 3 ACQUIRE_LOCK_IX OBJECT: 18:2073058421:0;ACQUIRE_LOCK_IX PAGE: 18:1:154; ACQUIRE_LOCK_X RID: 18:1:154:1
LOP_COMMIT_XACT LCX_NULL NULL NULL NULL NULL
Ровно то, чего и ожидали: IX‑блокировка таблицы, IX‑блокировка страницы и две X‑блокировки строк на этой странице.
А теперь что‑нибудь посложнее, например TRUNCATE TABLE. Слышали миф, что он «не журналается»? Вот именно — миф:
CHECKPOINT;
GO
TRUNCATE TABLE LockLogTest;
GO
SELECT [Operation], [Context], [Page ID], [Slot ID], [Number of Locks] AS Locks, [Lock Information]
FROM fn_dblog (NULL, NULL);
GO
Operation Context Page ID Slot ID Locks Lock Information
---------------- ------------- -------------- -------- ----- ----------------------------------------------------------------------------
LOP_BEGIN_XACT LCX_NULL NULL NULL NULL NULL
LOP_LOCK_XACT LCX_NULL NULL NULL 1 ACQUIRE_LOCK_SCH_M OBJECT: 18:2073058421:0
LOP_MODIFY_ROW LCX_IAM 0001:0000009b 0 1 ACQUIRE_LOCK_X RID: 18:1:155:0
LOP_MODIFY_ROW LCX_PFS 0001:00000001 0 1 ACQUIRE_LOCK_X PAGE: 18:1:154
LOP_MODIFY_ROW LCX_PFS 0001:00000001 0 1 ACQUIRE_LOCK_X PAGE: 18:1:155
LOP_MODIFY_ROW LCX_IAM 0001:00000099 0 1 ACQUIRE_LOCK_X RID: 18:1:153:0
LOP_MODIFY_ROW LCX_PFS 0001:00000001 0 1 ACQUIRE_LOCK_X PAGE: 18:1:152
LOP_MODIFY_ROW LCX_PFS 0001:00000001 0 1 ACQUIRE_LOCK_X PAGE: 18:1:153
LOP_SET_BITS LCX_SGAM 0001:00000003 1 NULL NULL
LOP_SET_BITS LCX_GAM 0001:00000002 1 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000014 89 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000011 78 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000014 90 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000041 164 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000041 165 NULL NULL
LOP_COUNT_DELTA LCX_CLUSTERED 0001:00000041 166 NULL NULL
LOP_HOBT_DDL LCX_NULL NULL NULL NULL NULL
LOP_MODIFY_ROW LCX_CLUSTERED 0001:00000014 89 2 ACQUIRE_LOCK_IX OBJECT: 18:7:0;ACQUIRE_LOCK_X KEY: 18:458752 (0000c2681664)
LOP_HOBT_DDL LCX_NULL NULL NULL NULL NULL
LOP_MODIFY_ROW LCX_CLUSTERED 0001:00000014 90 2 ACQUIRE_LOCK_IX OBJECT: 18:7:0;ACQUIRE_LOCK_X KEY: 18:458752 (00007a581379)
LOP_MODIFY_ROW LCX_CLUSTERED 0001:00000011 78 2 ACQUIRE_LOCK_IX OBJECT: 18:5:0;ACQUIRE_LOCK_X KEY: 18:327680 (00001df3833b)
LOP_COMMIT_XACT LCX_NULL NULL NULL NULL NULL
Много журналирования и много блокировок. Если присмотреться к столбцу Context, видно, что операция изменяет карты распределения (IAM, PFS, SGAM, GAM), но блокировки берутся на страницы таблицы, а не на сами карты распределения — их лишь кратко блокируют (latch — внутренний, гораздо более лёгкий механизм синхронизации). Всё это происходит в момент, когда страницы таблицы освобождаются; движок хранения поступает так потому, что таблица небольшая, и он выбирает немедленно освободить всё место, вместо того чтобы складывать работу в очередь фоновой задачи отложенного удаления. См. мою статью Когда после TRUNCATE страницы станут доступны для повторного использования?, там это тоже обсуждается.
Никаких реальных операций над строками самой пользовательской таблицы не выполняется. Единственные строковые операции — внизу, на таблицах с object_id 7 и 5 (sys.sysallocunits и sys.sysrowsets), где обновляются счётчики страниц, ссылки на первую IAM и первую страницу для данной таблицы.
Надеюсь, это было полезно. В следующей статье обсудим компенсирующие журнальные записи (compensation log records) и то, как работает откат (rollback).

Комментариев нет:
Отправить комментарий