Read committed е вторият най-слаб от четирите нива на изолация, определени от стандарта SQL. Независимо от това, това е нивото на изолация по подразбиране за много машини за бази данни, включително SQL Server. Тази публикация от поредица за нивата на изолация и свойствата на ACID на транзакциите разглежда логическите и физически гаранции, действително осигурени от изолацията с четене.
Логически гаранции
Стандартът на SQL изисква транзакция, изпълняваща се при изолация за четене, да се чете само committed данни. Той изразява това изискване, като забранява феномена на едновременност, известен като мръсно четене. Мръсно четене възниква, когато транзакция чете данни, които са били записани от друга транзакция, преди тази втора транзакция да завърши. Друг начин да се изрази това е да се каже, че се получава мръсно четене, когато транзакция чете незаети данни.
Стандартът също така споменава, че транзакция, изпълняваща се при изолация на четене, може да срещне феномена на едновременност, известен като неповтарящо се четене ифантоми . Въпреки че много книги обясняват тези явления от гледна точка на това, че транзакцията може да види променени или нови елементи от данни, ако данните впоследствие бъдат препрочетени, това обяснение може да засили погрешното схващане че явленията на едновременност могат да се появят само в изрична транзакция, която съдържа множество изрази. Това не е така. Едно изявление без изрична транзакция е също толкова уязвима към неповторяемото четене и фантомните феномени, както ще видим скоро.
Това е почти всичко, което стандартът има да каже по темата за изолацията при четене. На пръв поглед четенето само на въведени данни изглежда като доста добра гаранция за разумно поведение, но както винаги дяволът е в детайла. Веднага щом започнете да търсите потенциални вратички в тази дефиниция става твърде лесно да се намерят случаи, в които нашите транзакции за четене може да не произведат резултатите, които може да очакваме. Отново ще ги обсъдим по-подробно след момент-два.
Различни физически реализации
Има поне две неща, които означават, че наблюдаваното поведение на нивото на изолация за четене може да бъде доста различно в различните машини за бази данни. Първо, изискването на стандарта на SQL за четене само на заети данни не непременно означава, че ангажираните данни, прочетени от транзакция, ще бъдат най-скорошните ангажирани данни.
На машината за база данни е позволено да чете ангажиментирана версия на ред от всякакъв момент в миналото , и все още отговаря на стандартната дефиниция на SQL. Няколко популярни продукта за бази данни прилагат изолация за четене по този начин. Резултатите от заявките, получени при това внедряване на изолация за четене, може да са произволно неактуални , в сравнение с текущото ангажирано състояние на базата данни. Ще разгледаме тази тема, тъй като тя е приложима за SQL Server в следващата публикация от поредицата.
Второто нещо, на което искам да обърна внимание е, че стандартната дефиниция на SQL не не позволява на конкретна реализация да предоставя допълнителни защити с ефект на паралелност освен предотвратяването на мръсно четене . Стандартът само уточнява, че мръсните четения не са разрешени, той не изисква други явления на едновременност трябва да бъдат разрешени на всяко дадено ниво на изолация.
За да бъде наясно с тази втора точка, съвместим със стандартите механизъм за база данни може да приложи всички нива на изолация, използвайки сериализиращи се поведение, ако е избрало така. Някои големи търговски машини за бази данни също предоставят реализация на четене, което е далеч отвъд простото предотвратяване на мръсни четения (въпреки че никой не стига до осигуряване на пълна изолация в ACID смисъл на думата).
В допълнение към това, за няколко популярни продукта, прочетете committed изолацията е най-ниска налично ниво на изолация; техните реализации на read uncommitted изолацията са абсолютно същите като при четене. Това е позволено от стандарта, но тези видове различия добавят сложност към и без това трудната задача за мигриране на код от една платформа към друга. Когато говорим за поведението на ниво на изолация, обикновено е важно да се посочи и конкретната платформа.
Доколкото знам, SQL Server е уникален сред големите търговски двигатели за бази данни в предоставянето на две реализации на нивото на изолация за четене, всяко с много различно физическо поведение. Тази публикация обхваща първата от тях, заключване четете ангажирано.
Заключване на четене на SQL сървър е извършено
Ако опцията за база данни READ_COMMITTED_SNAPSHOT
е OFF
, SQL Server използва заключване внедряване на нивото на изолация на четене, при което се предприемат споделени заключвания, за да се предотврати едновременна транзакция да модифицира едновременно данните, тъй като модификацията изисква изключително заключване, което не е съвместимо със споделеното заключване.
Основната разлика между заключването на извършено четене на SQL Server и заключването на повторяемото четене (което също приема споделени ключалки при четене на данни) е, че при четене е извършено освобождаване на споделеното заключване възможно най-скоро , докато повторяемото четене държи тези ключалки до края на обграждащата транзакция.
Когато заключването на извършеното прочитане придобие заключвания при детайлност на реда, споделеното заключване, взето на ред, се освобождава когато се вземе споделено заключване на следващия ред . При детайлност на страницата споделеното заключване на страница се освобождава, когато се прочете първият ред на следващата страница и т.н. Освен ако със заявката не е предоставен намек за заключване на детайлност, машината на базата данни решава с какво ниво на детайлност да започне. Имайте предвид, че подсказките за детайлност се третират само като предложения от двигателя, възможно е първоначално да се използва по-малко детайлно заключване от поисканото. Заключването може също да бъде ескалирано по време на изпълнение от ниво ред или страница до ниво дял или таблица в зависимост от конфигурацията на системата.
Важният момент тук е, че споделените брави обикновено се задържат само за много кратко време докато изявлението се изпълнява. За да се справим изрично с едно често срещано погрешно схващане, заключването на извършено четене не задръжте споделените ключалки до края на изявлението.
Заключване на приетите поведения при четене
Краткосрочните споделени заключвания, използвани от реализацията на SQL Server за заключване при четене, предоставят много малко от гаранциите, които обикновено се очакват от транзакция на база данни от T-SQL програмистите. По-специално, изявление, което се изпълнява под заключване read commited изолация:
- Може да срещне един и същ ред няколко пъти;
- Може напълно да пропусне някои редове; и
- Не не предоставят изглед в момента от данните
Този списък може да изглежда по-скоро като описание на странното поведение, което може да свържете повече с използването на NOLOCK
намеци, но всички тези неща наистина могат и се случват при използване на изолация за заключване при четене.
Пример
Помислете за простата задача да преброите редовете в таблица, като използвате очевидната заявка с един израз. При заключваща изолация за четене, ангажирана изолация с детайлност на заключване на редове, нашата заявка ще вземе споделено заключване на първия ред, ще го прочете, освободи споделеното заключване, ще премине към следващия ред и така нататък, докато достигне края на структурата, която тя чете. В името на този пример приемете, че нашата заявка чете индексно b-дърво във възходящ ред на ключове (въпреки че може да използва и низходящ ред или всяка друга стратегия).
Тъй като само един ред е заключена за споделяне във всеки даден момент от време, очевидно е възможно едновременните транзакции да променят отключените редове в индекса, който нашата заявка обхожда. Ако тези едновременни модификации променят стойностите на ключа на индекса, те ще накарат редовете да се движат в рамките на структурата на индекса. Имайки предвид тази възможност, диаграмата по-долу илюстрира два проблемни сценария, които могат да възникнат:
Най-горната стрелка показва ред, който вече сме преброили, като неговият индексен ключ е променен едновременно, така че редът да се движи пред текущата позиция на сканиране в индекса, което означава, че редът ще бъде броен два пъти . Втората стрелка показва ред, който нашето сканиране не е срещнало, все още се движи зад позицията на сканиране, което означава, че редът няма да бъде отчетен изобщо.
Не е изглед към момента
Предишният раздел показа как заключването на извършеното четене може да пропусне напълно данни или да преброи един и същ елемент няколко пъти (повече от два пъти, ако нямаме късмет). Третата точка в списъка с неочаквани поведения гласи, че заключването на извършено четене също не предоставя изглед на данните в даден момент.
Разсъжденията зад това твърдение вече трябва да бъдат лесни за разглеждане. Нашата заявка за броене, например, може лесно да прочете данни, които са били вмъкнати от едновременни транзакции, след като нашата заявка е започнала да се изпълнява. По същия начин данните, които нашата заявка вижда, могат да бъдат променени от едновременна дейност след стартиране на нашата заявка и преди да завърши. И накрая, данните, които сме прочели и преброили, може да бъдат изтрити от едновременна транзакция, преди нашата заявка да завърши.
Ясно е, че данните, които се виждат от изявление или транзакция, изпълнявани при заключваща изолация за четене, отговарят на няма едно състояние от базата данни във всеки конкретен момент от време . Данните, които срещаме, може да са от различни моменти от време, като единственият общ фактор е, че всеки елемент представлява най-новата ангажирана стойност на тези данни в момента на четенето им (въпреки че може да се е променило или изчезнало оттогава).
Колко сериозни са тези проблеми?
Всичко това може да изглежда като доста мрачно състояние на нещата, ако сте свикнали да мислите за вашите заявки с един оператор и изрични транзакции като за логично изпълнение мигновено или като работещи срещу едно-единствено ангажирано състояние на базата данни, когато използвате ниво на изолация по подразбиране на SQL Server. Със сигурност не се вписва добре с концепцията за изолация в смисъла на КИСЕЛИНА.
Като се има предвид очевидната слабост на гаранциите, предоставени от заключването на изолацията за извършване на четене, може да започнете да се чудите как някои на вашия производствен T-SQL код някога е работил правилно! Разбира се, можем да приемем, че използването на ниво на изолация под сериализирано означава, че се отказваме от пълна изолация на транзакции ACID в замяна на други потенциални ползи, но колко сериозни можем да очакваме тези проблеми да бъдат на практика?
Липсващи и двойно преброени редове
Тези първи два проблема по същество разчитат на едновременна дейност, променяща ключовете в индексна структура, която в момента сканираме. Имайте предвид, че сканиране тук включва частта за сканиране на частичния диапазон на търсене на индекс , както и познатото неограничено сканиране на индекс или таблица.
Ако сканираме (диапазон) структура на индекс, чиито ключове обикновено не се променят от никаква едновременна дейност, тези първи два проблема не би трябвало да представляват голям практически проблем. Трудно е да бъдете сигурни за това, тъй като плановете за заявка могат да се променят, за да използват различен метод за достъп, а новият търсен индекс може да включва нестабилни ключове.
Също така трябва да имаме предвид, че много производствени заявки наистина се нуждаят само от приблизителна или все пак най-добрият отговор на някои видове въпроси. Фактът, че някои редове липсват или се отчитат двойно, може да няма голямо значение в по-широката схема на нещата. В система с много едновременни промени може дори да е трудно да сте сигурни, че резултатът е неточни, като се има предвид, че данните се променят толкова често. В такъв тип ситуация, приблизително правилният отговор може да е достатъчно добър за целите на потребителя на данни.
Без изглед във времето
Третият въпрос (въпросът за т. нар. „последователен“ преглед на данните във времето) също се свежда до същия вид съображения. За целите на отчитането, когато несъответствията водят до неудобни въпроси от потребителите на данни, изгледът на моментна снимка често е за предпочитане. В други случаи видът несъответствия, произтичащи от липсата на изглед на данните към момента, може да бъде поносим.
Проблемни сценарии
Има и много случаи, в които изброените притеснения ще бъде важен. Например, ако пишете код, който налага бизнес правила в T-SQL трябва да внимавате да изберете ниво на изолация (или да предприемете друго подходящо действие), за да гарантирате коректност. Много бизнес правила могат да бъдат наложени с помощта на външни ключове или ограничения, където тънкостите на избора на ниво на изолация се обработват автоматично за вас от двигателя на базата данни. Като общо правило, използването на вградения набор за декларативна цялост функции е за предпочитане пред изграждането на ваши собствени правила в T-SQL.
Има друг широк клас заявки, които не налагат напълно бизнес правило само по себе си , но което въпреки това може да има неприятни последици, когато се изпълнява на нивото на изолация за заключено четене по подразбиране. Тези сценарии не винаги са толкова очевидни, колкото често цитираните примери за прехвърляне на пари между банкови сметки или за гарантиране, че салдото върху редица свързани сметки никога не пада под нулата. Например, разгледайте следната заявка, която идентифицира просрочените фактури като вход за някакъв процес, който изпраща строго формулирани напомнящи писма:
INSERT dbo.OverdueInvoices SELECT I.InvoiceNumber FROM dbo.Invoices AS INV WHERE INV.TotalDue > ( SELECT SUM(P.Amount) FROM dbo.Payments AS P WHERE P.InvoiceNumber = I.InvoiceNumber );
Ясно е, че не бихме искали да изпращаме писмо до някой, който е платил изцяло фактурата си на вноски, просто защото едновременната активност на базата данни по време на изпълнението на нашата заявка означаваше, че изчислихме неправилна сума на получените плащания. Истинските заявки за реални производствени системи често са много по-сложни от простия пример по-горе, разбира се.
За да завършите за днес, разгледайте следната заявка и вижте дали можете да забележите колко възможности има нещо непреднамерено да се случи, ако няколко такива заявки се изпълняват едновременно на ниво на изолация за заключване на четене (може би докато други несвързани транзакции също така променят таблицата на случаите):
-- Allocate the oldest unallocated case ID to -- the current case worker, while ensuring -- the worker never has more than three -- active cases at once. UPDATE dbo.Cases SET WorkerID = @WorkerID WHERE CaseID = ( -- Find the oldest unallocated case ID SELECT TOP (1) C2.CaseID FROM dbo.Cases AS C2 WHERE C2.WorkerID IS NULL ORDER BY C2.DateCreated DESC ) AND ( SELECT COUNT_BIG(*) FROM dbo.Cases AS C3 WHERE C3.WorkerID = @WorkerID ) < 3;
След като започнете да търсите всички малки начини, по които една заявка може да се обърка на това ниво на изолация, може да е трудно да спрете. Имайте предвид предупрежденията, отбелязани по-рано около реалната нужда от напълно изолирани и точни резултати в даден момент. Напълно добре е да имате заявки, които връщат достатъчно добри резултати, стига да сте наясно с компромисите, които правите, като използвате read committed.
Следващия път
Следващата част от тази поредица разглежда втората физическа реализация на изолация с ангажимент за четене, налична в SQL Server, изолация на моментна снимка за четене.
[ Вижте индекса за цялата серия ]