версійність в Yukon h2>
Іван Бодягін p>
Вступ h2>
Чергову версію СУБД Microsoft SQL Server, що є
одним з лідерів ринку, чекали досить довго і, схоже, не дарма. У цьому продукті
тільки перелік нововведень буде досить важким, а вже повний опис
нових можливостей потягне на цілу книгу. Наразі доступна лише
альфа-версія продукту, а реліз очікується приблизно через рік, але, тим не менше,
вже за цією версією можна зрозуміти, що нас очікує в майбутньому. У цій статті
описується тільки одне, але досить серйозне нововведення, а саме
підтримка версійність. Цю функціональність спробували вмонтувати в класичний
блокуючий сервер (далі - блокіровочнік), і дуже цікаво подивитися, що
ж з цього вийшло. p>
Загальні принципи h2>
Паралельне виконання транзакцій здатне призвести
базу в неузгоджене стан навіть у тих випадках, коли кожна транзакція,
що виконується окремо від інших, робить повністю коректні зміни.
Тому черговість операцій різних транзакцій повинна тим чи іншим чином
регулюватися. p>
У всіх попередніх версіях Microsoft SQL Server
механізм подібної регулювання був заснований на блокування. Проте в новій версії
(кодова назва Yukon) буде введена підтримка іншого механізму, заснованого
на контролі версій (multiversioning). Надалі два цих підходу я буду
називати версійність і блокувальним відповідно. p>
версійність сама по собі, не є новим словом в
способи забезпечення коректності паралельної обробки транзакцій,
теоретичні роботи були ще на початку вісімдесятих, та й поява перших
комерційних реалізацій відноситься приблизно до того ж часу. Однак до
останнього моменту розробники Microsoft SQL Server послідовно
вдосконалювали блокувальний механізм, отримавши в результаті одну із найбільш вдалих
реалізацій класичного блокіровочніка в індустрії, а от тепер, схоже,
дійшли руки і до версійність. Привабливість даного механізму полягає в
те, що читають запити ніяк не заважають які пишуть, і навпаки. Але ніщо не
дається задарма. Втім, про все по порядку. p>
Від механізму паралельної обробки транзакцій
формальна теорія вимагає одного: щоб кінцевий ефект від паралельного
виконання було таким, як ніби-то транзакції виконувалися послідовно, при
цьому порядок їх проходження фактично не важливий. При дотриманні цієї умови,
якщо всі транзакції самі по собі є правильна, то і їх паралельне виконання
цілісність бази жодним чином не порушить. Дана умова має назву «критерію
впорядкованості ». p>
Способів дотримання вищезазначеного критерію
існує досить багато. Крім вже згадуваних механізмів, заснованих на
блокування і зберіганні версій, існує ще мало не десяток інших. Більше
того, існує й формальний доказ того, що цим критерієм можна
дотримуватися, використовуючи і комбіновані підходи, застосовуючи різні механізми для
різних типів запитів. Знову ж таки відомі і досить успішні практичні
реалізації таких гібридів. p>
Рівні ізоляції
h2>
Критерій впорядкованості усім хороший, крім одного --
суворе дотримання йому занадто дорого обходиться з точки зору
продуктивності. Але, оскільки даний критерій є достатньою, але не
необхідною умовою коректності паралельної обробки транзакцій, то в
залежно від характеру транзакцій можна підвищити ступінь їх втручання в
роботу один одного без сумних наслідків. Щоб хоч якось формалізувати
ці втручання, було запроваджено так звані «рівні ізоляції» (Isolation
Level). P>
Якщо по-простому, то «рівень ізоляції» - це ступінь
паралелізму транзакцій. У стандарті ANSI SQL рівні ізоляції введені
за допомогою феноменів - небажаних побічних ефектів від зайвого
паралелізму, таким чином, що кожен більш високий (більш суворий) рівень
ізоляції усуває черговий феномен, а також не допускає прояви
феноменів, вже усунених більш низьким (менш суворим) рівнем. p>
У стандарті описані чотири рівні ізоляції: p>
read uncommitted - читання незафіксованих
( «Брудних») даних. Це найнижчий рівень ізоляції. Він лише гарантує, що
не відбудеться феномена «брудної записи» (Dirty Write). Але при цьому рівні
ізоляції цілком можливий феномен «брудне читання» (Dirty Read). Припустимо,
перший транзакція записала якісь дані в X і не зафіксувалася, тобто
дані в X не зафіксовано. Якщо інша транзакція ці дані прочитає, а
потім першим буде скасована з якоїсь причини, то вийде, що друга
транзакція прочитала дані, які ніколи не існували. p>
read committed - читання зафіксованих даних.
Гарантується, що відбувається читання тільки зафіксованих даних. Тобто
такого неподобства, як у попередньому прикладі, не відбудеться. Але зате, якщо однією
транзакції потрібно два рази прочитати якісь дані, і між двома читаннями
вклинитися інша транзакція, яка встигне ці дані поміняти або вставити
нові, і зафіксуватися, то два читання одних і тих же даних в одній
транзакції будуть відрізнятися. p>
repeatable read. Проблему двох послідовних читань
одних і тих самих даних цей рівень ізоляції вирішує. Однак якщо між двома
читаннями в базу додалися нові дані, що задовольняють того ж критерію, за
яким проводилося перше читання, то ці нові дані вдруге так само
прочитають. Цей феномен носить назву «Фантомне читання» (Phantom read). P>
serializability. Найвищий рівень ізоляції. Він не
спирається ні на які феномени, а прямо формулюється із критерію
впорядкованості. Тобто при даному рівні ізоляції ніякі феномен не
можливі за визначенням, тому що феномен - це порушення послідовного
виконання транзакцій, що в даному випадку неможливо. p>
Однак за таку класифікацію стандарт піддавався
неодноразової і, загалом-то, справедливій критиці. Справа в тому, що в дану
градацію ідеально вписуються тільки чисті блокіровочнікі, але якщо застосовується
трохи інший спосіб забезпечення паралелізму, то його вже досить
проблематично звести до цих чотирьох рівнях, та й не завжди потрібно. Інші
механізми забезпечення паралелізму можуть допускати інші феномени, бути трохи
суворіше або трохи слабше. За великим рахунком, незмінним залишається лише одне
вимога - гарантія впорядкованості, все інше - сірі зони з нечіткими
кордонами, які дуже сильно залежать від деталей конкретної реалізації. p>
Розберемо спочатку коротко класичний блокіровочнік, а
потім розглянемо, що принесла версійність. p>
блокувальний механізм h2>
Принцип дії, в общем-то, ясен з назви - в
основі лежить протокол двофазної блокування. Перед читанням або зміною
об'єкт (запис) блокується. Тобто іншим транзакцій забороняється змінювати
або навіть читати цей об'єкт до тих пір, поки першим транзакція не закінчить з
ним працювати. p>
Рівень ізоляції read committed забезпечується за рахунок
того, що читають запити в транзакції не утримують своїх блокувань до
кінця транзакції, а знімають їх відразу ж, після прочитання. Таким чином, якщо
read committed-транзакція двічі прочитає один і той самий об'єкт, то його
значення може відрізнятися, тому що ніщо не завадить іншої транзакції змінити
його в проміжку між двома читаннями. p>
За рівня ізоляції repeatable read читають запити
утримують свої блокування до кінця транзакції, але вони блокують безліч
реальних записів, що існують на початок транзакції, а не записів, що відповідають
умові вибірки, які можуть з'явитися під час життя транзакції. Наприклад,
якщо вибрати всі записи, де x = 2, то на всіх цих записах буде утримуватися
блокування та поміняти їх буде не можна. Але ніщо не завадить додати в іншій
транзакції ще кілька записів з x = 2, і друга вибірка записів з цим же
умовою в першу транзакції поверне, в тому числі, і ці додані записи. p>
Рівень ізоляції serializable забезпечується
накладенням так званих предикативних блокувань. Це означає, що блокування
накладається не тільки на об'єкт, а й на умову. Якщо брати попередній
приклад, то ми не зможемо додати запис з x = 2 в іншій транзакції, якщо перша
зробила вибірку по цій умові, так само як умова x = 2 виявилося
заблокованим. Таким чином, навіть повторне читання будь-яких даних в першій
транзакції завжди буде повертати один і той же результат. p>
Більш докладно про блокування можна прочитати в статті
«Механізм блокувань Microsoft SQL Server 2000» в третьому номері RSDN Magazine
за 2003 рік. Зараз більший інтерес представляє версійність механізм. P>
версійність механізм
h2>
Принцип дії версійність заснований на тому, що
транзакція, змінюючи дані, породжує нову копію (версію) даних, з якою і
працює. Іншим транзакцій ця версія не видно, до тих пір, поки перша не
зафіксується. При цьому навіть після фіксації перших транзакції, застаріла
версія якийсь час зберігається для коректної роботи транзакцій,
стартували до завершення роботи першої, але ще не встигли зафіксуватися. p>
Для тих, хто читає запитів все працює дуже красиво і
ефективно. Вони просто отримують узгоджений зріз даних на момент початку
транзакції або запиту, але для пишучих запитів і транзакцій все не так просто.
Якщо дві транзакції вирішать змінити один і той самий об'єкт, то виникне конфлікт
версій. Перемагає та транзакція, яка встигла першою, а запізнилась, як
правило, доводиться відкочувати. З точки зору продуктивності відкат
досить-таки дорога операція, до того ж доводиться в обов'язковому порядку
передбачати обробку подібного конфлікту. Якщо в чистому блокіровочніке
відкат транзакції явне наслідок помилки, то в версіонніке відкат може відбутися
в цілком невинної ситуації. p>
Що стосується рівнів ізоляції в версійність моделі, то
вони можуть трансформуватися приблизно в наступне. p>
Read uncommitted. У чистому версіонніке зазвичай не
реалізований, тому що «брудні» дані незафіксованих транзакцій іншим
транзакціям не видно, та й не навіщо. p>
Read committed. Цей рівень ізоляції кілька
суворіше, ніж у моделі, заснованої на блокування. У блокіровочніке, якщо
читаюча транзакція з рівнем ізоляції read committed наткнеться на вже
змінений, але ще не зафіксований об'єкт, то вона буде чекати його
зміни і прочитає вже змінені дані. У версіонніке ж читає запит, як
правило, бере версію даних на момент початку запиту, тому вибірка буде
узгодженою. При цьому не важливо, чи змінюються дані в даний момент
будь-якої сторонньої транзакцією чи ні. Проте повторний запит тих же
даних, якщо їх встигли поміняти між двома запитами, поверне вже змінені
значення. Тому тут можна спостерігати той же самий ефект неповторюваних
читання, що і в блокувальним read committed. Зміни ж даних при цьому
рівні ізоляції в версіонніке, в плані узгодженості, мало чим відрізняються від
аналогічних дій блокіровочніка, так як змінюватися має все одно
остання зафіксована версія даних, а не застаріла копія. Однак
невеликі відмінності, про які буде розказано пізніше, все ж таки є. p>
Repeatable read. Як правило, версіоннікі цей рівень
ізоляції не підтримують, оскільки досить легко реалізується більш суворий
рівень ізоляції, який крім неповторним читання усуває і поява
фантомів. p>
Snapshot. Цього рівня ізоляції немає в класифікації
ANSI, він присутній тільки в версіонніках. Суть його в тому, що при цьому
рівні ізоляції забезпечується узгоджений зріз даних на момент початку
транзакції, ніякі зміни, що відбулися після старту транзакції в ній не
видно. Іншими словами, робиться те ж саме, що і в Read committed, але не для
одного запиту, а для транзакції в цілому. Таким чином, виключаються й
неповторні читання, і фантомні читання. З записом даних при цьому рівні
ізоляції все трохи складніше, тому що якщо транзакція при змінах
виявляє, що необхідні їй дані вже змінив хтось інший, то її
доводиться відкочувати. p>
Serializable. Хоча попередній рівень ізоляції в
версіонніке усуває практично всі можливі феномени, але, тим не менше,
ймовірність невпорядкованості як і раніше залишається, тому необхідність у
даному рівні ізоляції зберігається. У класичному версіонніке впорядкованість
досягається за рахунок комбінації snapshot-рівня ізоляції та фіктивного зміни
деяких записів, щоб їх не змінили інші транзакції. У гібридних системах,
як правило, можна обійтися і меншою кров'ю. p>
Реалізація в Yukon
h2>
У Yukon версійність не є станом сервера в
цілому, вона може бути включена для кожної бази окремо, причому за
замовчуванням версійність включена лише для службових БД master і msdb, і
тестової AdventureWorks. p>
версійність включається за допомогою нехитрої команди: p>
ALTER
DATABASE database_name SET ALLOW_SNAPSHOT_ISOLATION ON p>
Після її виконання сервер не відразу перемикає базу в
версійність режим, а переводить механізм підтримки версійність (snapshot
isolation framework) у стан PENDING_ON, оскільки в цей момент в базі
можуть бути активні транзакції. Після завершення усіх активних транзакцій над
базою проводяться всі необхідні зміни, механізм версійність для неї
переводиться в стан ON, і з'являється можливість виконувати версійність
запити. Зворотна дія здійснюється також у два етапи, спочатку БД
переводиться в стан PENDING_OFF, а потім вже відключається механізм підтримки
версійність. p>
Перевірити стан усіх баз на сервері можна з
допомогою ось такого запиту: p>
SELECT
name, snapshot_isolation_framework FROM sys.databases p>
Тут «snapshot_isolation_framework» може приймати такі значення: 0 - OFF, 1 - ON, 2
- PENDING_OFF, 3 - PENDING_ON. P>
Механізм підтримки версійність відповідає за збереження
копій даних, вибірку потрібних версій при версійність запитах, автоматичне
визначення конфліктів при змінах і виконує іншу рутинну роботу з
підтримки версійність. Він є не окремою службою або модулем, а
відносно простим набором алгоритмів, які включаються в роботу при
перекладі бази у відповідний стан. p>
Фізично механізм версійність в Yukon відстежує
зміна даних окремо для кожного запису. Для цього до запису додається
шматочок довжиною в 14 байт, в якому зберігається ідентифікатор транзакції,
змінила цей запис, і посилання на попередню зафіксовану версію. Таким
чином, всі версії одного запису утворюють зв'язний список, і серверу в разі потреби
не складає ніяких труднощів спуститися по ланцюжку посилань до потрібних даних. p>
ПРИМІТКА p>
У тому випадку, якщо на сторінці даних
не вистачає місця під додаткові 14 байт на кожен запис, то під час
перекладу бази в «версійність» стан дані будуть автоматично
перерозподілені по сторінках. p>
Всі версії запису збираються в спеціальному сховищі
(version store heaps), яке розташоване в tempdb. При будь-яку зміну запису
попередня версія копіюється у це сховище. p>
Завдяки особливостям tempdb і відсутності
журналювання для сховища версій при обслуговуванні та читанні копій даних,
навантаження на операції введення/виводу обіцяє бути мінімальною. p>
Ті застарілі копії даних, які вже точно не будуть
потрібні ні однієї транзакції, видаляються з tempdb автоматично. При цьому
відстежується найстаріша зацікавлена транзакція. Тому виключається
ймовірність того, що сервер в якийсь момент не знайде потрібну версію.
Видалення виробляє спеціальний механізм, який враховує поточну навантаження,
через кількість активних версійність транзакцій, і відсоток вільного місця в
tempdb. p>
Для оцінки передбачуваного розміру сховища версій
пропонується така формула: Size (KB) = частота оновлення версій (Version
generation rate) Kb/sec. * Час виконання найдовшою транзакції (the
longest transaction time) sec. p>
Як це працює
h2>
Тут знову ж таки простіше за все розібрати по черзі всі
рівні ізоляції пропоновані Yukon знизу вгору. p>
Read uncommitted
p>
У чистому версіонніке, як уже говорилося, read
uncommitted зазвичай не передбачений, та й не потрібен. У Yukon при запитах до БД з
включеною підтримкою версійність такий фокус теж провернути не вийде.
Навіть при явному вказівці відповідних підказок оптимізатору у запиті,
повертається попередня зафіксована версія, а не брудні дані транзакції,
заблокувала запис від змін. p>
Read committed
p>
З read committed все трохи складніше. Після включення підтримки версійність всі запити на
читання при цьому рівні ізоляції автоматично починають працювати як версійність.
То есть якщо транзакція при читанні натикається на заблоковану запис, то вона
читає попередню версію цього запису з tempdb. p>
ПРИМІТКА p>
Насправді в alpha-версії Yukon для
підтримки версійність при read committed необхідно включити спеціальний
Трейс-прапор (3970). Але Microsoft урочисто клянеться, що у фінальному
продукті все буде відбуватися автоматично. p>
Можна провести простенький експеримент. Хай є
невелика табличка tst в БД з підтримкою версійність, наприклад, AdventureWork,
створена за допомогою ось такого скрипта: p>
CREATE TABLE tst (x int, y int) p>
GO p>
INSERT INTO tst (x, y) VALUES (1, 5) p>
INSERT INTO tst (x, y) VALUES (2, 4) p>
INSERT INTO tst (x, y) VALUES (3, 3) p>
INSERT INTO tst (x, y) VALUES (4, 2) p>
INSERT INTO tst (x, y) VALUES (5, 1) p>
Спочатку відкриємо нове підключення, відкриємо read
committed-транзакцію, і зробимо вибірку, транзакцію при цьому закривати не будемо. p>
SET
TRANSACTION ISOLATION LEVEL READ COMMITTED p>
BEGIN
TRAN p>
SELECT * FROM tst WHERE x = 3 p>
Отримаємо те, що і очікувалося: x = 3, y = 3. p>
Тепер в іншій транзакції спробуємо відновити цю
запис, також не фіксуючи транзакцію. p>
BEGIN
TRAN p>
UPDATE tst SET y = -1 WHERE x = 3 p>
Якщо після цього глянути на блокування, накладені
на табличку tst, то, як і при використанні попередніх версій SQL Server,
можна помітити ексклюзивну блокування на запис і два блокування наміри
вище за ієрархією, на сторінку і таблицю. p>
Тип p>
Опис p>
Об'єкт p>
Режим p>
Статус p>
spid p>
TAB p>
1963154039 p>
IX p>
GRANT p>
52 p>
RID p>
1:1357:2 p>
72057594057326592 p>
X p>
GRANT p>
52 p>
PAG p>
1:1357 p>
72057594057326592 p>
IX p>
GRANT p>
52 p>
Таблиця 1 p>
Тобто картина збігається з тією, яку можна бачити
при використанні попередньої версії SQL Server або БД без підтримки
версійність. p>
Проте далі починаються відмінності. Якщо зараз
повернутися до першої транзакції і спробувати знову виконати той же самий запит,
то він абсолютно спокійно відпрацює. p>
SELECT
* FROM tst WHERE x = 3 p>
І результат буде таким самим: x = 3, y = 3. Якщо
спробувати зробити те ж саме на БД без включеної підтримки версійність, то
другий запит з першої транзакції не виконається. Він буде чекати фіксації або
відкоту другу транзакції. Тобто він просто не зможе прочитати потрібну
запис, оскільки вона заблокована. Але в даному випадку блокування анітрохи не
заважає прочитати версію даних, що існувала на момент початку вибірки. p>
Більш того, в силу особливостей роботи з
неіндексірованнимі таблицями (а для тестової таблиці індексів не створювалося),
в базі без підтримки версійність другий запит в перший транзакції не зміг би
вибрати не тільки заблоковану
запис, а й будь-яку іншу. Через блокування йому все одно б довелося чекати
завершення роботи першої транзакції. Говорячи простіше, у блокіровочніка, у разі
відсутність індексів, блокування одного запису перетворюється фактично в
блокування всієї таблиці. p>
Якщо тепер зафіксувати зміни тестової таблиці,
вироблені другу транзакцією: p>
COMMIT TRAN p>
і зробити знову вибірку тих же даних в транзакції
номер один: p>
SELECT
* FROM tst WHERE x = 3 p>
то ми вже отримаємо результат x = 3, y = (-1), феномен
неповторного читання (non-repeatable read) у дії. Нам вдалося двічі
звернутися до одних і тих же даних з однієї транзакції і отримати різні
результати. p>
Отже, для тих, хто читає запитів все працює просто чудово,
перейдемо тепер до пишучим запитам все при тому ж рівні ізоляції. p>
Припустимо, версіоннік намагається оновити кілька
записів з read committed-транзакції і натикається на те, що хто-то в даний
момент з іншої транзакції оновив, але ще не зафіксував одну з потрібних
записів. Просто почекати, поки ворожа транзакція зафіксується, і потім
поміняти запис, не можна, тому що до цього часу запис може бути змінена
таким чином, що перестане задовольняти критеріям запиту. Щоб цього не
відбулося, необхідно, як мінімум, перечитати цей запис заново - щоб
змінювати актуальну версію, а не застарілу. p>
Таким чином виникає певний парадокс - при одному
і тому ж рівні ізоляції читають запити виходять більш злагоджено, ніж
друкарські. Формально все в порядку - вимоги рівня ізоляції не порушені.
Щоб уникнути такого парадоксу, в деяких комерційних реалізаціях в таких
випадках робиться відкат запиту, а потім запит виконується заново, щоб
забезпечити оновлення на узгодженому зрізі даних. p>
Розробники Yukon не стали додавати таких
складностей, і всі зміни робляться по-старому, точно так само, як їх робить
звичайний блокіровочнік. Аж до побічного ефекту, пов'язаного з блокуванням
всієї таблиці через відсутність індексу. p>
Якщо, наприклад, в одному підключенні виконати частину
транзакції, змінивши в тестовій табличці один запис, але не фіксувати
транзакцію, утримуючи тим самим блокування ... p>
SET TRANSACTION ISOLATION LEVEL READ COMMITTED p>
BEGIN TRAN p>
UPDATE tst SET y = 3 WHERE x = 3 p>
А в іншій транзакції спробувати змінити іншу
запис ... p>
SET
TRANSACTION ISOLATION LEVEL READ COMMITTED p>
BEGIN
TRAN p>
UPDATE tst SET y =- 1 WHERE x = 4 p>
COMMIT p>
Те друге транзакція не завершиться, а подвіснет в
задумі. Якщо ж у цей момент поглянути на блокування, накладені на
таблицю tst: p>
Тип p>
Опис p>
Об'єкт p>
Режим p>
Статус p>
spid p>
PAG p>
1:1357 p>
72057594057326592 p>
IU p>
GRANT p>
54 p>
PAG p>
1:1357 p>
72057594057326592 p>
IX p>
GRANT p>
53 p>
RID p>
1:1357:2 p>
72057594057326592 p>
X p>
GRANT p>
53 p>
RID p>
1:1357:2 p>
72057594057326592 p>
U p>
WAIT p>
54 p>
TAB p>
1963154039 p>
IX p>
GRANT p>
54 p>
TAB p>
1963154039 p>
IX p>
GRANT p>
53 p>
Таблиця 2 p>
то можна спостерігати картину, характерну для самого
звичайного блокіровочніка, хоча версійність ніхто не відміняв, у чому можна
переконатися, виконавши в ще однієї паралельної read committed-транзакції читає
запит: p>
SET
TRANSACTION ISOLATION LEVEL READ COMMITTED p>
BEGIN
TRAN p>
SELECT * FROM tst p>
COMMIT p>
Цей запит абсолютно спокійно виконається, нікого не
потривоживши, і поверне при цьому стан таблиці на момент, що передує
змін, тому що ні одна з змінюють таблицю транзакцій на час
виконання цього запиту ще не зафіксовано. Вся справа в тому, що зазвичай
версіоннік виконує зміни даних приблизно за таким сценарієм: p>
Робиться версійність сканування таблиці, і
з'ясовується, які записи потрібно змінити. p>
Робляться спроби змінити відібрані записи. p>
Якщо запис змінилася з моменту версійність скана, то
перевіряється, чи вона не перестала задовольняти критерієм відбору, і якщо не
перестала, то запис міняється, якщо перестала, то пропускається. (Деякі
реалізації застосовують тут більш хитрі алгоритми, але це питання виходить за
рамки цієї статті). p>
Якщо запис міняється зараз, то сервер
чекає кінця змін і знову ж таки перевіряє відповідність запису умов
вибірки. p>
Тобто очікування, якщо і відбувається, то тільки через
того, що в процесі оновлення зустрілася запис, який змінюється в даний
момент. p>
Чистий ж блокіровочнік працює трохи по іншому
сценарієм. Сканування даних йому не має сенсу робити, тому що всі запити
на читання в нього блокують. Тому він просто перебирає всі записи в таблиці
по черзі (нагадаю, мова йде про таблицю без індексів), перевіряючи їх на
відповідність умові вибірки, і накладаючи при цьому блокування оновлення
(update lock). Таке блокування сумісна з блокуваннями читання, але
несумісна сама з собою і з монопольними блокуваннями. Таким чином,
переглядає його запитам подібний перебір не заважає, але інші блокування оновлення та
монопольні будуть перешкодою цьому запиту. Отже, якщо в момент перебору
в таблиці монопольно заблокована хоча б один запис (що й має місце в
даному прикладі, так як запис була змінена, але транзакція ще не
зафіксована), то рано чи пізно змінює запит до неї добереться і зависне
на блокування, чекаючи фіксації «ворожої» транзакції. p>
Незважаючи на можливість версійність запитів, Yukon все
одно при записі даних надходить як блокіровочнік, що і призводить до
вищеописаному ефекту. p>
Repeatable read
p>
Рівень ізоляції repeatable read в базі з включеною
підтримкою версійність працює точно так само, як і на базі без неї.
Абсолютно спокійно накладаються і утримуються належний час всі покладені
за статусом колективні (share) блокування. Та взагалі-то, навряд чи тут взагалі
щось могло змінитися. Але з'явилася одна корисна можливість: Якщо запит
виконується по базі з включеною підтримкою версійність, то за умов згадування
оптимізатору Хінта READCOMMITTED в читаючої запиті, вибірка буде версійність.
Можливість дійсно досить корисна - у зв'язку з деякими
особливостями рівня ізоляції snapshot. p>
Snapshot
p>
Рівень ізоляції snapshot, є чисто версійність,
на відміну від попереднього, чисто блокувального, і взагалі абсолютно новим для
SQL Server. p>
читають запити при цьому рівні ізоляції виконуються
так, як і належить їм виконуватися в чесному версіонніке при цьому рівні
ізоляції. Якщо повернутися все до тієї ж тестової табличці і в одному з
підключень почати транзакцію, в якій змінити яку-небудь запис, але саму
транзакцію не фіксувати ... p>
BEGIN
TRAN p>
UPDATE tst SET y = 2 WHERE x = 4 p>
А в іншому підключенні почати snapshot транзакцію з
переглядає його запитом до тієї ж табличці ... p>
SET
TRANSACTION ISOLATION LEVEL SNAPSHOT p>
BEGIN
TRAN p>
SELECT
* FROM tst p>
Те snapshot-транзакція, як, втім, і версійність
read committed, абсолютно спокійно відпрацює, повернувши попереднє значення
зміненої запису. Однак якщо зараз зафіксувати першим, змінює
транзакцію ... p>
COMMIT TRAN p>
А потім повторити ту ж саму вибірку з snapshot
транзакції ... p>
SELECT * FROM tst p>
То ця вибірка поверне все ще старі значення записів,
що існували до фіксації перші транзакції. Тобто тут читання повністю
відтворено, на відміну від read committed. Більше того, цей рівень ізоляції
не допускає появи фантомів, на відміну від блокувального repeatable read.
Наприклад, якщо виконати третім транзакцію, в якій до таблиці додаються
записи ... p>
BEGIN
TRAN p>
INSERT INTO tst (x, y) VALUES (6, 0) p>
COMMIT TRAN p>
Те чергова вибірка всіх записів таблиці tst з
snapshot-транзакції поверне все ті ж записи, що і в перший раз, просто тому,
що на момент першого запиту нового значення ще не було. Загалом, з таблицею
можна робити все що завгодно, все вибірки з snapshot-транзакції будуть одними і тими
ж. p>
Таким чином, читання при рівні ізоляції snapshot в
Yukon практично нічим не відрізняється від звичайного версійність. Воно працює по
тими ж принципами, і «зовнішній» ефект точно такий же. Транзакція отримує
узгоджений зріз даних, починаючи з першого звернення до даних, і все
подальші зміни її не стосуються. p>
Все це дуже добре працює при читають запитах,
однак при записі можуть виникати конфлікти. Якщо при виконанні оновлення
snapshot-транзакція добереться до запису, заблокованої інший транзакцією,
те, виникне конфлікт версій. Якщо блокуюча транзакція успішно фіксується,
в чистому версіонніке snapshot-транзакція відкочується, оскільки якщо вона
змінить дані більш «молодої» транзакції і продовжить роботу, цілком можливий
феномен загубленого оновлення. Змістити ці транзакції один щодо одного
в часі і вважати snapshot-транзакцію більш «молодої» теж не вийде, тому
як блокуюча транзакція могла додати записи, що задовольняють умові
вибірки snapshot-транзакції, а значить, все snapshot-запити повинні були ці
запису побачити. Тобто snapshot-транзакція все одно повинна виконатися
заново, з більш пізньої тимчасовою міткою, щоб побачити всі зміни,
внесені блокує транзакцією. p>
І в даному випадку Yukon мало чим відрізняється від
версіонніка. Якщо при рівні ізоляції read committed у разі зміни він
може собі дозволити поводитися як блокіровочнік, то при рівні ізоляції
snapshot такий фокус не пройде. Як мінімум при цьому snapshot скотиться все до
того ж read committed. Справа в тому, що блокіровочнік рівні ізоляції вище read
committed забезпечує утриманням колективних (share) блокувань при запитах
на читання до кінця транзакції. Версіоннік ж подібних блокувань для
забезпечення рівня ізоляції snapshot не використовує, у нього принцип зовсім
інший. А оскільки до моменту конфлікту snapshot-транзакція вже могла виконати
кілька версійність читань, то поступати як блокіровочнік вже пізно, тому
Yukon, так само як і версіоннік, у випадку конфлікту відкочується snapshot-транзакцію. P>
Якщо продовжити знущання над таблицею tst, і
змінити в ній яку-небудь запис, не фіксуючи транзакцію: p>
BEGIN
TRAN p>
UPDATE tst SET y = -1 WHERE x = 3 p>
а потім спробувати змінити ту ж запис з
snapshot-транзакції: p>
SET
TRANSACTION ISOLATION LEVEL SNAPSHOT p>
BEGIN
TRAN p>
UPDATE tst SET y = 3 WHERE x = 3 p>
COMMIT TRAN p>
Те snapshot-транзакція зависне в задумі. При
погляді на блокування, накладені на таблицю tst, можна спостерігати картину,
характерну для звичайного блокіровочіка. p>
Тип p>
Опис p>
Об'єкт p>
Режим p>
Статус p>
spid p>
TAB p>
1963154039 p>
IX p>
GRANT p>
51 p>
RID p>
1:1357:2 p>
72057594057326592 p>
U p>
WAIT p>
51 p>
PAG p>
1:1357 p>
72057594057326592 p>
IU p>
GRANT p>
51 p>
TAB p>
1963154039 p>
IX p>
GRANT p>
52 p>
RID p>
1:1357:2 p>
72057594057326592 p>
X p>
GRANT p>
52 p>
PAG p>
1:1357 p>
72057594057326592 p>
IX p>
GRANT p>
52 p>
Таблиця 3 p>
Snapshot-транзакція (spid 52) очікує на блокування (U
- WAIT), поки звільниться потрібна запис (RID 1:1357:2), заблокована інший
транзакцією (spid 51) монопольно (X - GRANT). p>
Якщо зараз повернутися в першу вікно і відкинути
блокує транзакцію, то snapshot абсолютно спокійно виконає свою
оновлення і зафіксується. Однак якщо блокує транзакцію зафіксувати,
то, на відміну від блокувального поведінки, snapshot-транзакція буде скасовано,
і клієнтський додаток отримає повідомлення про помилку: p>
. Net
SqlClient Data Provider: Msg 3960, Level 16, State 1, Line 1 p>
Cannot
use snapshot isolation to access table 'tst' in database 'AdventureWorks'. p>
Snapshot
transaction aborted due to update conflict. Retry transaction. P>
Більш того, для відкату snapshot-транзакції їй навіть не
потрібно очікувати зняття блокування. Щоб уникнути неузгодженого зміни,
необхідно проводити відкат навіть у тому випадку, якщо після старту
snapshot-транзакції один із записів, необхідних для пише запиту, була
змінена інший транзакцією, що встигла зафіксуватися. p>
Якщо в одному з підключень почати
snapshot-транзакцію, зробивши просту вибірку: p>
SET
TRANSACTION ISOLATION LEVEL SNAPSHOT p>
BEGIN
TRAN p>
SELECT
* FROM tst p>
Потім в іншому підключенні змінити яку-небудь
запис: p>
BEGIN
TRAN p>
UPDATE tst SET y = 3 WHERE x = 3 p>
COMMIT TRAN p>
А потім спробувати змінити цей же запис з
snapshot-транзакції: p>
UPDATE tst SET
y = 3 WHERE x = 3 p>
COMMIT TRAN p>
Те snapshot транзакція успіхом не увінчається, а буде
скасована через конфлікт версій. p>
Справа в тому, що якщо існує транзакція,
зафіксована після початку роботи snapshot-транзакції, і зміни цієї
транзакції потрапляють в умови вибірки, зробленої snapshot-транзакцією, то
цілком можуть існувати записи, які були змінені після цієї транзакцією
того, як snapshot-запит їх переглянув, таким чином, що вони стали
задовольняти умові snapshot-вибірки. Тобто виходить, що
snapshot-транзакція може побачити тільки частина змін, зроблених інший транзакцією,
а цього допустити не можна. p>
У даному випадку поведінка Yukon дуже схоже на
версійність. Але насправді він все одно веде себе трохи
«По-блокувального». Виявляється це, наприклад, все в тій же ситуації з
відсутністю індексів. p>
Якщо ще раз виконати що блокує дію, не
фіксуючи її: p>
BEGIN
TRAN p>
UPDATE tst SET y= 3 WHERE x = 3 p>
А потім спробувати змінити іншу запис з snapshot
транзакції: p>
SET
TRANSACTION ISOLATION LEVEL SNAPSHOT p>
BEGIN
TRAN p>
UPDATE tst SET y =- 1 WHERE x = 4 p>
COMMIT TRAN p>
Те snapshot транзакція все одно виявиться
заблокованою. p>
Тип p>
Опис p>
Об'єкт p>
Режим p>
Статус p>
spid p>
RID p>
1:1357:2 p>
72057594057326592 p>
U p>
WAIT p>
51 p>
Таблиця 4 p>
І, що характерно, блокування відбудеться на тій же
самої запису. Іншими словами, у відсутності індексів Yukon здійснює пошук по
таблиці як самий звичайний блокіровочнік, навіть при рівні ізоляції snapshot.
Версіоннік в даній ситуації спочатку сканує всю таблицю звичайним версійність
запитом, з'ясовуючи потрібні записи, а потім звертається з оновленням тільки до тих,
які підходять під умову вибірки, тому подібного там не відбувається.
Yukon ж сканує таблицю, накладаючи блокування оновлення на кожен запис,
і знімаючи її, якщо запис не задовольняє умові вибірки, або конвертуючи в
ексклюзивну, якщо задовольняє. Таким чином, навіть якщо заблокована
запис не задовольняє умові вибірки оновлюючої оператора
snapshot-транзакції, у разі відсутності індексів транзакція все одно
застрягне на блокування. p>
Результат подібного очікування snapshot-транзакції
цілком закономірний. Якщо блокує транзакцію зафіксувати, то
snapshot-транзакція буде скасовано, незважаючи на те, що формально змінена
запис ні до, ні після запиту під умови вибірки не потрапляла. Обертається
це тим, що якщо в момент зміни записів у таблиці без індексів з
snapshot-транзакції буде змінена будь-який запис, то snapshot-транзакція не
виконається. p>
Судячи з усього, зробити узгоджене неблокірующее
сканування при оновленні записів - завдання нетривіальна. Тому все зроблено
одноманітно, «по-блокувального». З одного боку, подібне блокувальний
сканування обіцяє досить похмурі перспективи спробам запису з
snapshot-транзакцій в неіндексірованние таблиці. Але, з іншого боку, це
запобігає деякі екзотичні випадки фантомів, які пропускає
класичний snapshot. p>
Serializable
p>
Незважаючи на те, що версійність snapshot допомагає
позбавитися від більшості побічних ефектів, пов'язаних з паралельним
виконанням транзакцій, ймовірність появи деяких феноменів, як і раніше
залишається. Щоб позбутися від подібних ефектів, при роботі з класичним
версіонніком і деякими гібридами доводиться вживати додаткові
заходу у вигляді фіктивних змін або спеціальних блокувань деяких записів. p>
Але Yukon, як і попередні версії SQL Server від
Microsoft, підтримує рівень ізоляції serializable, який за визначенням
не допускає жодних феноменів. За механізмом цей рівень ізоляції є
чисто блокувальним і ніякі версійність запити, навіть на читання, тут не
підтримуються, якщо звичайно, не давати спеціальних вказівок оптимізатору. p>
Висновок
p>
Нова функціональність, безумовно, надамо