Read uncommitted е най-слабото от четирите нива на изолация на транзакции, дефинирани в SQL Standard (и от шестте, внедрени в SQL Server). Позволява и трите така наречени „явления на едновременност“, мръсно четене , неповторими четения , и фантоми:
Повечето хора в базата данни са запознати с тези явления, поне в общи линии, но не всеки осъзнава, че не описват напълно предлаганите гаранции за изолация; нито пък интуитивно описват различните поведения, които човек може да очаква в конкретна реализация като SQL Server. Повече за това по-късно.
Изолиране на транзакции – 'I' в ACID
Всяка SQL команда се изпълнява в рамките на транзакция (явна, неявна или автоматично извършване). Всяка транзакция има свързано ниво на изолация, което определя колко е изолирана от ефектите на други едновременни транзакции. Тази донякъде техническа концепция има важни последици за начина, по който се изпълняват заявките и качеството на резултатите, които произвеждат.
Помислете за проста заявка, която брои всички редове в таблица. Ако тази заявка може да бъде изпълнена мигновено (или с нулеви едновременни модификации на данни), може да има само един верен отговор:броят на редовете, физически присъстващи в таблицата в този момент във времето. В действителност изпълнението на заявката ще отнеме известно време и резултатът ще зависи от това колко реда действително среща машината за изпълнение, докато преминава през всяка физическа структура, избрана за достъп до данните.
Ако редовете се добавят към (или се изтриват от) таблицата чрез едновременни транзакции, докато операцията за броене е в ход, могат да се получат различни резултати в зависимост от това дали транзакцията за преброяване на редове среща всички, някои или нито една от тези едновременни промени - което от своя страна зависи от нивото на изолация на транзакцията за броене на редове.
В зависимост от нивото на изолация, физическите детайли и времето на едновременните операции, нашата транзакция за броене може дори да доведе до резултат, който никога не е бил вярно отражение на състоянието на ангажимента на таблицата във всеки един момент от време по време на транзакцията.
Пример
Помислете за транзакция с броене на редове, която започва в момент T1 и сканира таблицата от начало до край (в ред на клъстерни индексни ключове, за целта на аргумента). В този момент в таблицата има 100 ангажирани реда. Известно време по-късно (в момент T2) нашата транзакция за броене е срещнала 50 от тези редове. В същия момент, едновременна транзакция вмъква два реда в таблицата и записва кратко време по-късно в момент T3 (преди преброяването на транзакцията да приключи). Един от вмъкнатите редове попада в половината от клъстерираната индексна структура, която нашата транзакция за броене вече е обработила, докато другият вмъкнат ред се намира в непреброената част.
Когато транзакцията за преброяване на редове завърши, тя ще отчете 101 реда в този сценарий; 100 реда първоначално в таблицата плюс единичният вмъкнат ред, който е бил открит по време на сканирането. Този резултат е в противоречие с историята на ангажиментите на таблицата:имаше 100 записани реда в моменти T1 и T2, след това 102 записани реда в момент T3. Никога не е имало момент, в който да има 101 заети реда.
Изненадващото нещо (може би, в зависимост от това колко задълбочено сте мислили за тези неща преди) е, че този резултат е възможен при стандартното (заключващо) ниво на изолация на ангажимент за четене и дори при повторяема изолация на четене. И двете нива на изолация гарантират, че четат само ангажирани данни, но получихме резултат, който не представлява състояние на ангажимент на базата данни!
Анализ
Единственото ниво на изолация на транзакциите, което осигурява пълна изолация от ефекти на паралелност, може да се сериализира. Реализацията на SQL Server на сериализиращото се ниво на изолация означава, че транзакцията ще види най-новите ангажирани данни от момента, в който данните са били заключени за първи път за достъп. В допълнение, наборът от данни, срещан при изолация с възможност за сериализиране, гарантирано няма да промени членството си, преди транзакцията да приключи.
Примерът за преброяване на редове подчертава фундаментален аспект на теорията на базата данни:трябва да сме наясно какво означава "правилен" резултат за база данни, която изпитва едновременни модификации, и трябва да разберем компромисите, които правим, когато избираме изолация ниво по-ниско от това, което може да се сериализира.
Ако се нуждаем от изглед в момента на състоянието на ангажимент на базата данни, трябва да използваме изолация на моментни снимки (за гаранции на ниво транзакция) или да прочетем изолация на ангажирани моментни снимки (за гаранции на ниво оператор). Обърнете внимание обаче, че изгледът към момента означава, че не е задължително да работим с текущото ангажирано състояние на базата данни; всъщност може да използваме остаряла информация. От друга страна, ако сме доволни от резултатите, базирани само на записани данни (макар и вероятно от различни моменти във времето), бихме могли да изберем да се придържаме към нивото на изолация на заключеното по подразбиране при четене, ангажирано.
За да сме сигурни, че ще произвеждаме резултати (и вземаме решения!) въз основа на най-новия набор от ангажирани данни, за някаква серийна история на операции срещу базата данни, ще ни трябва сериализираща се изолация на транзакции. Разбира се, тази опция обикновено е най-скъпата от гледна точка на използване на ресурсите и по-ниска едновременност (включително повишен риск от блокиране).
В примера за преброяване на редове и двете нива на изолация на моментни снимки (SI и RCSI) биха дали резултат от 100 реда, представляващи броя на заети редове в началото на оператора (и транзакцията в този случай). Изпълнението на заявката при заключване на извършено четене или изолиране на повтарящо се четене може да доведе до резултат от 100, 101 или 102 реда – в зависимост от времето, детайлността на заключване, позицията на вмъкване на ред и избрания физически метод за достъп. При изолация с възможност за сериализиране резултатът ще бъде или 100, или 102 реда, в зависимост от това коя от двете едновременни транзакции се счита за изпълнена първа.
Колко лошо е Read Uncommitted?
След като въведохте изолацията без ангажимент за четене като най-слабото от наличните нива на изолация, трябва да очаквате, че тя ще предложи дори по-ниски гаранции за изолация, отколкото заключването на четене, ангажирано (следващото най-високо ниво на изолация). Наистина е така; но въпросът е:колко по-лошо от заключването на изолацията за четене е това?
За да започнем с правилния контекст, ето списък с основните ефекти на едновременност, които могат да бъдат изпитани при заключването по подразбиране на SQL Server за четене, ангажирано ниво на изолация:
- Липсващи ангажирани редове
- Редове, срещани няколко пъти
- Различни версии на един и същи ред се срещат в един план за изявление/заявка
- Данни за ангажименти в колони от различни моменти във времето в един и същи ред (пример)
Всички тези ефекти на паралелност се дължат на заключващата реализация на четене, ангажирано само с много краткосрочни споделени заключвания при четене на данни. Нивото на изолация на незаетите четения отива още една стъпка по-далеч, като изобщо не взема споделени заключване, което води до допълнителна възможност за „мръсно четене“.
Мръсно четене
Като бързо напомняне, „мръсно четене“ се отнася до четене на данни, които се променят от друга едновременна транзакция (където „промяната“ включва операции за вмъкване, актуализиране, изтриване и сливане). Казано по друг начин, мръсно четене възниква, когато транзакция чете данни, които друга транзакция е променила, преди променящата се транзакция да е извършила или прекъснала тези промени.
Предимства и недостатъци
Основните предимства на изолирането без ангажимент за четене са намаленият потенциал за блокиране и блокиране поради несъвместими заключвания (включително ненужно блокиране поради ескалация на заключване) и евентуално повишена производителност (чрез избягване на необходимостта от придобиване и освобождаване на споделени ключалки).
Най-очевидният потенциален недостатък на изолацията за четене без ангажимент е (както подсказва името), че може да четем незаети данни (дори данни, които никога ангажирани, в случай на връщане на транзакция). В база данни, където връщането е сравнително рядко, въпросът за четене на незаети данни може да се разглежда като обикновен проблем с времето, тъй като въпросните данни със сигурност ще бъдат ангажирани на някакъв етап и вероятно доста скоро. Вече видяхме несъответствия, свързани с времето в примера за преброяване на редове (който работеше на по-високо ниво на изолация), така че може да се запитаме колко опасно е да се четат данни „твърде рано“.
Очевидно отговорът зависи от местните приоритети и контекст, но информирано решение за използване на изолация без ангажимент за четене със сигурност изглежда възможно. Все пак има какво да се мисли. Внедряването на SQL Server на нивото на изолация без ангажимент за четене включва някои фини поведения, за които трябва да сме наясно, преди да направим този „информиран избор“.
Сканиране на поръчката за разпределение
Използването на изолация без ангажимент за четене се приема от SQL Server като сигнал, че сме готови да приемем несъответствията, които могат да възникнат в резултат на сканиране, подредено за разпределение.
Обикновено механизмът за съхранение може да избере подредено за разпределение сканиране само ако основните данни гарантирано няма да се променят по време на сканирането (тъй като например базата данни е само за четене или е посочен намек за заключване на таблица). Въпреки това, когато се използва изолация без ангажимент за четене, машината за съхранение все още може да избере сканиране, подредено за разпределение, дори когато основните данни могат да бъдат модифицирани от едновременни транзакции.
При тези обстоятелства сканирането, подредено за разпределение, може да пропусне напълно някои заети данни или да срещне други ангажиментирани данни повече от веднъж. Акцентът е върху липсващи или двойно отчитане на извършени данни (без четене на незаети данни), така че не става въпрос за "мръсно четене" като такова. Това дизайнерско решение (да се позволи сканиране, подредено за разпределение при изолация без ангажимент за четене) се разглежда от някои хора като доста противоречиво.
Като предупреждение, трябва да бъда ясен, че по-общият риск от липсващи или двойно отчитане на ангажименти редове не се ограничава до четене на неангажирани изолации. Със сигурност е възможно да се видят подобни ефекти при заключване на четене, ангажирано и повтарящо се четене (както видяхме по-рано), но това се случва чрез различен механизъм. Липсват заети редове или се срещат многократно поради сканиране, подредено за разпределение при промяна на данните е специфично за използване на изолация за четене без ангажимент.
Четене на „повредени“ данни
Резултати, които изглежда противоречат на логиката (и дори да проверяват ограниченията!), са възможни при изолация за заключване на четене (отново вижте тази статия от Крейг Фридман за някои примери). За да обобщим, въпросът е, че заключването на записаното четене може да види записани данни от различни моменти във времето – дори за един ред, ако например планът на заявката използва техники като пресичане на индекси.
Тези резултати може да са неочаквани, но са напълно в съответствие с гаранцията за четене само на ангажирани данни. Просто няма как да се измъкнем от факта, че по-високите гаранции за последователност на данните изискват по-високи нива на изолация.
Тези примери може дори да са доста шокиращи, ако не сте ги виждали преди. Едни и същи резултати са възможни при изолиране на незаетите четения, разбира се, но разрешаването на мръсно четене добавя допълнително измерение:резултатите може да включват ангажирани и незаети данни от различни моменти във времето, дори за един и същи ред.
Продължавайки по-нататък, дори е възможно за прочетена незаети транзакция да прочете стойност на една колона в смесено състояние на ангажирани и незаети данни. Това може да се случи при четене на LOB стойност (например xml или някой от типовете "max"), ако стойността се съхранява в множество страници с данни. Неангажирано четене може да срещне ангажирани или незаети данни от различни моменти във времето на различни страници, което води до крайна стойност в една колона, която е смесица от стойности!
За да вземем пример, помислете за една колона varchar(max), която първоначално съдържа 10 000 'x' знака. Едновременна транзакция актуализира тази стойност до 10 000 знака „y“. Прочетена незаети транзакция може да чете символи „x“ от една страница на LOB и символи „y“ от друга, което води до крайна прочетена стойност, съдържаща смес от символи „x“ и „y“. Трудно е да се спори, че това не представлява четене на „повредени“ данни.
Демо
Създайте клъстерирана таблица с един ред LOB данни:
CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, LOB varchar(max) NOT NULL, ); INSERT dbo.Test (RowID, LOB) VALUES (1, REPLICATE(CONVERT(varchar(max), 'X'), 16100));
В отделна сесия изпълнете следния скрипт, за да прочетете стойността на LOB при изолация без ангажимент за четене:
-- Run this in session 2 SET NOCOUNT ON; DECLARE @ValueRead varchar(max) = '', @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100), @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100); WHILE 1 = 1 BEGIN SELECT @ValueRead = T.LOB FROM dbo.Test AS T WITH (READUNCOMMITTED) WHERE T.RowID = 1; IF @ValueRead NOT IN (@AllXs, @AllYs) BEGIN PRINT LEFT(@ValueRead, 8000); PRINT RIGHT(@ValueRead, 8000); BREAK; END END;
В първата сесия изпълнете този скрипт, за да напишете редуващи се стойности в колоната LOB:
-- Run this in session 1 SET NOCOUNT ON; DECLARE @AllXs varchar(max) = REPLICATE(CONVERT(varchar(max), 'X'), 16100), @AllYs varchar(max) = REPLICATE(CONVERT(varchar(max), 'Y'), 16100); WHILE 1 = 1 BEGIN UPDATE dbo.Test SET LOB = @AllYs WHERE RowID = 1; UPDATE dbo.Test SET LOB = @AllXs WHERE RowID = 1; END;
След кратко време скриптът във втората сесия ще приключи, след като е прочел смесено състояние за LOB стойността, например:
Този конкретен проблем е ограничен до четене на стойности на LOB колони, които са разпределени в множество страници, не поради някакви гаранции, предоставени от нивото на изолация, а защото SQL Server използва ключалки на ниво страница, за да гарантира физическа цялост. Страничен ефект от тази подробност за внедряване е, че предотвратява подобни „повредени“ четения на данни, ако данните за една операция на четене се намират на една страница.
В зависимост от версията на SQL Server, която имате, ако данните за "смесено състояние" се прочетат за xml колона, вие или ще получите грешка, произтичаща от евентуално неправилен xml резултат, изобщо няма грешка или специфичната за неизпълнение грешка 601 , "не можа да продължи сканирането с NOLOCK поради движение на данни." Четенето на данни със смесено състояние за други типове LOB обикновено не води до съобщение за грешка; консумиращото приложение или заявка няма начин да разбере, че току-що е претърпяло най-лошия вид мръсно четене. За да завършите анализа, ред със смесено състояние без LOB, прочетен в резултат на пресичане на индекс, никога не се отчита като грешка.
Съобщението тук е, че ако използвате изолация за четене без ангажимент, вие приемате, че мръсното четене включва възможността за четене на "повредени" LOB стойности със смесено състояние.
Намекът за NOLOCK
Предполагам, че нито едно обсъждане на нивото на изолиране на неангажираните четени не би било пълно без поне да се спомене този (широко използван и неразбран) намек за таблицата. Самият намек е просто синоним на подсказката за таблица READUNCOMMITTED. Той изпълнява точно същата функция:обектът, към който се прилага, е достъпен с помощта на семантика на изолация за четене без ангажимент (въпреки че има изключение).
Що се отнася до името „NOLOCK“, то просто означава, че не се вземат споделени заключвания при четене на данни . Други заключвания (стабилност на схемата, изключителни заключвания за промяна на данни и т.н.) все още се приемат като нормални.
Най-общо казано, съветите за NOLOCK трябва да са толкова често срещани, колкото и други съвети за таблица за ниво на изолация на обект като SERIALIZABLE и READCOMMITTEDLOCK. Тоест:изобщо не е много често срещан и се използва само там, където няма добра алтернатива, добре дефинирана цел за нея и пълна разбиране на последствията.
Един пример за законно използване на NOLOCK (или READUNCOMMITTED) е при достъп до DMV или други системни изгледи, където по-високо ниво на изолация може да причини нежелана конкуренция върху структури от данни, които не са потребители. Друг пример за краен случай може да бъде, когато заявка трябва да получи достъп до значителна част от голяма таблица, която гарантирано никога няма да претърпи промени в данните, докато се изпълнява намекната заявка. Трябва да има основателна причина вместо това да не се използва изолация на моментна снимка или четене на ангажирани моментни снимки, а очакваните увеличения на производителността ще трябва да бъдат тествани, валидирани и сравнени, да речем, с помощта на единичен намек за заключване на споделена таблица.
Най-малко желаната употреба на NOLOCK е тази, която за съжаление е най-често срещаната:прилагането му към всеки обект в заявка като нещо като по-бърз магически превключвател. С най-добрата воля в света просто няма по-добър начин да направите кода на SQL Server да изглежда определено аматьорски. Ако законно се нуждаете от изолация за четене без ангажимент за заявка, кодов блок или модул, вероятно е по-добре да зададете подходящо ниво на изолация на сесията и да предоставите коментари, за да оправдаете действието.
Последни мисли
Read uncommitted е легитимен избор за ниво на изолация на транзакциите, но трябва да бъде информиран избор. Като напомняне, ето някои от явленията на едновременност, възможни при заключването по подразбиране на SQL Server за четене, ангажирана изолация:
- Липсващи по-рано ангажирани редове
- Срещани няколко пъти ангажименти
- Различни ангажирани версии на един и същ ред, срещани в един план за изявление/заявка
- Активирани данни от различни моменти във времето в същия ред (но различни колони)
- Прочетени данни за ангажименти, които изглежда противоречат на разрешени и проверени ограничения
В зависимост от вашата гледна точка, това може да е доста шокиращ списък с възможни несъответствия за нивото на изолация по подразбиране. Към този списък прочетете неангажирани изолационни добавки:
- Мръсно четене (среща се с данни, които все още не са, а може и никога да не бъдат, ангажирани)
- Редове, съдържащи смес от ангажирани и незаети данни
- Пропуснати/дублирани редове поради подредени по разпределение сканирания
- Смесено състояние („повредено“) индивидуални (една колона) LOB стойности
- Грешка 601 – „не можа да продължи сканирането с NOLOCK поради движение на данни“ (пример).
Ако основните ви притеснения за транзакциите са за страничните ефекти от заключването на изолацията за четене – блокиране, блокиране на режийни разходи, намален паралелизъм поради ескалация на заключване и т.н. – може да бъдете по-добре обслужени от ниво на изолация на версиите на редове, като изолация на моментна снимка, ангажирана с четене (RCSI) или изолиране на моментни снимки (SI). Те обаче не са безплатни и по-специално актуализациите под RCSI имат някакво противоинтуитивно поведение.
За сценарии, които изискват най-високи нива на гаранции за последователност, сериализируемостта остава единственият безопасен избор. За критични за производителността операции с данни само за четене (например големи бази данни, които ефективно са само за четене между ETL прозорци), изричното задаване на базата данни на READ_ONLY също може да бъде добър избор (споделените заключвания не се вземат, когато базата данни е само за четене и няма риск от несъответствие).
Ще има и сравнително малък брой приложения, за които изолацията за четене без ангажимент е правилният избор. Тези приложения трябва да са доволни от приблизителни резултати и възможността за понякога непоследователни, очевидно невалидни (по отношение на ограниченията) или „вероятно повредени“ данни. Ако данните се променят сравнително рядко, рискът от тези несъответствия също е съответно по-нисък.
[ Вижте индекса за цялата серия ]