Проблемите с паралелността са трудни по същия начин, както е трудно многонишковото програмиране. Освен ако не се използва сериализираща се изолация, може да е трудно да се кодират T-SQL транзакции, които винаги ще функционират правилно, когато други потребители правят промени в базата данни по едно и също време.
Потенциалните проблеми могат да бъдат нетривиални, дори ако въпросната „транзакция“ е обикновена единична SELECT
изявление. За сложни транзакции с множество оператори, които четат и записват данни, потенциалът за неочаквани резултати и грешки при висок едновременност може бързо да стане огромен. Опитът за разрешаване на фини и трудни за възпроизвеждане проблеми с едновременността чрез прилагане на произволни намеци за заключване или други методи за проба-грешка може да бъде изключително разочароващо изживяване.
В много отношения нивото на изолация на моментни снимки изглежда като перфектно решение на тези проблеми с едновременността. Основната идея е, че всяка транзакция на моментна снимка се държи така, сякаш е изпълнена срещу собствено частно копие на ангажимента на състоянието на базата данни, взето в момента на стартиране на транзакцията. Предоставянето на цялата транзакция с непроменен изглед на ангажираните данни очевидно гарантира последователни резултати за операции само за четене, но какво да кажем за транзакциите, които променят данните?
Изолирането на моментни снимки обработва промените в данните оптимистично, като имплицитно предполага, че конфликтите между едновременно пишещи ще бъдат относително редки. Когато възникне конфликт при записване, първият комитър печели и на губещата транзакция се отменят промените. Разбира се, това е жалко за отменената транзакция, но ако това е достатъчно рядко явление, ползите от изолирането на моментни снимки лесно могат да надвишават разходите за случайна грешка и повторен опит.
Относително простата и изчистена семантика на изолирането на моментни снимки (в сравнение с алтернативите) може да бъде значително предимство, особено за хора, които не работят изключително в света на базата данни и следователно не познават добре различните нива на изолация. Дори за опитни професионалисти по бази данни, относително „интуитивното“ ниво на изолация може да бъде добре дошло облекчение.
Разбира се, нещата рядко са толкова прости, колкото изглеждат на пръв поглед, и изолирането на моментни снимки не е изключение. Официалната документация върши доста добра работа за описване на основните предимства и недостатъци на изолирането на моментни снимки, така че по-голямата част от тази статия се концентрира върху изследването на някои от по-малко известните и изненадващи проблеми, които може да срещнете. Първо, обаче, бърз поглед върху логическите свойства на това ниво на изолация:
Свойства на ACID и изолиране на моментна снимка
Изолирането на моментна снимка не е едно от нивата на изолация, дефинирани в SQL стандарта, но все още често се сравнява с помощта на дефинираните там „явления на паралелност“. Например, следната сравнителна таблица е възпроизведена от техническата статия на SQL Server, "SQL Server 2005 Row Versioning Based Transaction Isolation" от Кимбърли Л. Трип и Нийл Грейвс:
Чрез предоставяне на изглед в момента от ангажирани данни , изолирането на моментни снимки осигурява защита срещу трите показани там едновременно явления. Мръсните четения са предотвратени, тъй като се виждат само въведените данни, а статичното естество на моментната снимка предотвратява срещата както на неповтарящи се четения, така и на фантоми.
Въпреки това, това сравнение (и подчертаният раздел в частност) показва само, че моментната снимка и нивата на изолация, които могат да се сериализират, предотвратяват същите три специфични явления. Това не означава, че са еквивалентни във всички отношения. Важно е, че стандартът SQL-92 не дефинира сериализираща се изолация само по отношение на трите явления. Раздел 4.28 от стандарта дава пълната дефиниция:
Изпълнението на едновременни SQL-транзакции на ниво изолация SERIALIZABLE е гарантирано, че може да се сериализира. Изпълнение, което може да се сериализира, се дефинира като изпълнение на операциите на едновременно изпълнявани SQL транзакции, което произвежда същия ефект като някакво серийно изпълнение на същите тези SQL транзакции. Серийното изпълнение е такова, при което всяка SQL-транзакция се изпълнява до завършване, преди да започне следващата SQL-транзакция.
Степента и значението на подразбиращите се гаранции тук често се пропускат. За да го изкажете на прост език:
Всяка сериализираща се транзакция, която се изпълнява правилно, когато се изпълнява самостоятелно, ще продължи да се изпълнява правилно с всяка комбинация от едновременни транзакции или ще бъде върната обратно със съобщение за грешка (обикновено блокиране в реализацията на SQL Server).
Нивата на изолация, които не могат да се сериализират, включително изолацията на моментна снимка, не осигуряват същите силни гаранции за коректност.
Застарели данни
Изолирането на моментна снимка изглежда почти съблазнително просто. Четенията винаги идват от записани данни в един момент и конфликтите при запис се откриват и обработват автоматично. Как това не е идеално решение за всички проблеми, свързани с едновременността?
Един потенциален проблем е, че четенията на моментни снимки не отразяват непременно текущото състояние на ангажимента на базата данни. Транзакцията за моментна снимка напълно игнорира всички ангажименти промени, направени от други едновременни транзакции, след като транзакцията за моментна снимка започне. Друг начин да се каже това е да се каже, че транзакцията за моментна снимка вижда остарели, неактуални данни. Въпреки че това поведение може да е точно това, което е необходимо за генериране на точен отчет за момента, то може да не е толкова подходящо при други обстоятелства (например, когато се използва за налагане на правило в задействане).
Изкривяване на запис
Изолирането на моментна снимка също е уязвимо към донякъде свързано явление, известно като изкривяване при запис. Четенето на остарели данни играе роля в това, но този проблем също помага да се изясни какво прави и какво не прави „откриването на конфликт при записване“ на моментна снимка.
Изкривяване при запис възниква, когато две едновременни транзакции, всяка от които чете данни, които другата транзакция модифицира. Не възниква конфликт при записване, защото двете транзакции променят различни редове. Нито една транзакция не вижда промените, направени от другата, защото и двете четат от момент, преди тези промени да бъдат направени.
Класически пример за изкривяване при писане е проблемът с белия и черния мрамор, но искам да покажа друг прост пример тук:
-- Create two empty tables CREATE TABLE A (x integer NOT NULL); CREATE TABLE B (x integer NOT NULL); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT A (x) SELECT COUNT_BIG(*) FROM B; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT B (x) SELECT COUNT_BIG(*) FROM A; COMMIT TRANSACTION; -- Connection 1 COMMIT TRANSACTION;
При изолация на моментна снимка и двете таблици в този скрипт завършват с един ред, съдържащ нулева стойност. Това е правилен резултат, но не може да се сериализира:не съответства на нито една възможна поръчка за изпълнение на серийни транзакции. Във всеки наистина сериен график една транзакция трябва да завърши преди да започне другата, така че втората транзакция ще отчита реда, вмъкнат от първата. Това може да звучи като техническо дело, но не забравяйте, че мощните гаранции за сериализиране се прилагат само когато транзакциите наистина могат да се сериализират.
Тънкост при откриване на конфликт
Конфликт при запис на моментна снимка възниква всеки път, когато транзакция на моментна снимка се опитва да промени ред, който е бил променен от друга транзакция, която е извършена след началото на транзакцията за моментна снимка. Тук има две тънкости:
- Транзакциите всъщност не трябва да се променят всякакви стойности на данните; и
- Транзакциите не трябва да променят никакви общи колони .
Следният скрипт демонстрира и двете точки:
-- Test table CREATE TABLE dbo.Conflict ( ID1 integer UNIQUE, Value1 integer NOT NULL, ID2 integer UNIQUE, Value2 integer NOT NULL ); -- Insert one row INSERT dbo.Conflict (ID1, ID2, Value1, Value2) VALUES (1, 1, 1, 1); -- Connection 1 BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value1 = 1 WHERE ID1 = 1; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value2 = 1 WHERE ID2 = 1; -- Connection 1 COMMIT TRANSACTION;
Обърнете внимание на следното:
- Всяка транзакция намира един и същ ред, използвайки различен индекс
- Нито една актуализация не води до промяна на вече съхранените данни
- Двете транзакции „актуализират“ различни колони в реда.
Въпреки всичко това, когато първата транзакция извърши, втората транзакция завършва с грешка при конфликт на актуализация:
Резюме:Откриването на конфликт винаги работи на ниво цял ред и „актуализация“ не трябва да променя действително никакви данни. (В случай, че се чудите, промените в LOB или SLOB данни извън реда също се считат за промяна на реда за целите на откриването на конфликти).
Проблемът с външния ключ
Откриването на конфликт се отнася и за родителския ред във връзка с външен ключ. Когато модифицирате дъщерен ред при изолация на моментна снимка, промяна на родителския ред в друга транзакция може да предизвика конфликт. Както и преди, тази логика се прилага за целия родителски ред – родителската актуализация не трябва да засяга самата колона с външен ключ. Всяка операция върху дъщерната таблица, която изисква автоматична проверка на външния ключ в плана за изпълнение, може да доведе до неочакван конфликт.
За да демонстрирате това, първо създайте следните таблици и примерни данни:
CREATE TABLE dbo.Dummy ( x integer NULL ); CREATE TABLE dbo.Parent ( ParentID integer PRIMARY KEY, ParentValue integer NOT NULL ); CREATE TABLE dbo.Child ( ChildID integer PRIMARY KEY, ChildValue integer NOT NULL, ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent ); INSERT dbo.Parent (ParentID, ParentValue) VALUES (1, 1); INSERT dbo.Child (ChildID, ChildValue, ParentID) VALUES (1, 1, 1);
Сега изпълнете следното от две отделни връзки, както е посочено в коментарите:
-- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.Dummy; -- Connection 2 (any isolation level) UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1; -- Connection 1 UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1; UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;
Четенето от фиктивната таблица е там, за да се гарантира, че транзакцията за моментна снимка е започнала официално. Издаване на BEGIN TRANSACTION
не е достатъчно за това; трябва да извършим някакъв вид достъп до данни на потребителска таблица.
Първата актуализация на дъщерната таблица не води до конфликт, тъй като задаване на референтната колона на NULL
не изисква проверка на родителска таблица в плана за изпълнение (няма какво да се проверява). Процесорът на заявки не докосва родителския ред в плана за изпълнение, така че не възниква конфликт.
Втората актуализация на дъщерната таблица наистина задейства конфликт, тъй като проверката на външния ключ се извършва автоматично. Когато родителският ред е достъпен от процесора на заявки, той също се проверява за конфликт на актуализация. В този случай възниква грешка, тъй като посоченият родителски ред е претърпял извършена модификация след стартиране на транзакцията за моментна снимка. Имайте предвид, че модификацията на родителската таблица не е засегнала самата колона с външен ключ.
Може да възникне и неочакван конфликт, ако промяна в дъщерната таблица препраща към родителски ред, който е създаден чрез едновременна транзакция (и тази транзакция, извършена след стартиране на транзакцията за моментна снимка).
Резюме:План за заявка, който включва автоматична проверка на външния ключ, може да доведе до грешка в конфликта, ако посоченият ред е претърпял някаква модификация (включително създаване!) след стартиране на транзакцията за моментна снимка.
Проблемът с отрязаната таблица
Транзакцията за моментна снимка ще се провали с грешка, ако някоя таблица, до която има достъп, е била съкратена от началото на транзакцията. Това важи дори ако съкратената таблица нямаше редове за начало, както показва скриптът по-долу:
CREATE TABLE dbo.AccessMe ( x integer NULL ); CREATE TABLE dbo.TruncateMe ( x integer NULL ); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.AccessMe; -- Connection 2 TRUNCATE TABLE dbo.TruncateMe; -- Connection 1 SELECT COUNT_BIG(*) FROM dbo.TruncateMe;
Крайният SELECT се проваля с грешка:
Това е друг фин страничен ефект, който трябва да проверите, преди да активирате изолирането на моментни снимки в съществуваща база данни.
Следващия път
Следващата (и последна) публикация от тази поредица ще говори за нивото на изолация на прочетеното необвързано (нежно известно като "nolock").
[ Вижте индекса за цялата серия ]