Database
 sql >> база данни >  >> RDS >> Database

Модификации на данни при изолация на четене на ангажирани моментни снимки

[ Вижте индекса за цялата серия ]

Предишната публикация от тази поредица показа как T-SQL изявление работи при изолация на прочетени, ангажирани моментни снимки (RCSI ) обикновено вижда моментна снимка на състоянието на ангажимента на базата данни, както е било, когато операторът е започнал да се изпълнява. Това е добро описание как работят нещата за изявления, които четат данни, но има важни разлики за изрази, изпълнявани под RCSI, които модифицират съществуващи редове .

Наблягам на модификацията на съществуващи редове по-горе, тъй като следните съображения важат само за UPDATE и DELETE операции (и съответните действия на MERGE изявление). За да бъде ясно, INSERT изявленията са специално изключени от поведението, което ще опиша, защото вмъкванията не променят съществуващите данни.

Актуализиране на заключванията и версиите на редове

Първата разлика е, че изразите за актуализиране и изтриване не четат версиите на редове под RCSI при търсене на изходните редове за промяна. Актуализирайте и изтривайте изявления под RCSI вместо това придобиват заключване за актуализиране при търсене на квалифицирани редове. Използването на заключвания за актуализиране гарантира, че операцията за търсене намира редове, които да промени, използвайки най-скорошните заети данни .

Без заключвания за актуализиране, търсенето ще се основава на евентуално остаряла версия на набора от данни (ангажирани данни, каквито са били, когато е започнал изявлението за модификация на данните). Това може да ви напомни за примера за задействане, който видяхме последния път, където READCOMMITTEDLOCK hint беше използван за връщане от RCSI към заключващата реализация на изолация, завършена за четене. Този намек беше необходим в този пример, за да се избегне базирането на важно действие върху остаряла информация. Тук се използва същият вид разсъждение. Една разлика е, че READCOMMITTEDLOCK hint придобива споделени заключвания вместо заключвания за актуализиране. Освен това SQL Server автоматично придобива заключвания за актуализиране, за да защити модификациите на данни под RCSI, без да изисква от нас да добавяме изричен намек.

Приемането на заключвания за актуализиране също така гарантира, че изявлението за актуализиране или изтриване ще блокира ако срещне несъвместимо заключване, например изключително заключване, защитаващо модификация на данните по време на полет, извършена от друга едновременна транзакция.

Допълнително усложнение е, че модифицираното поведение прилага само към таблицата, която е целта на операцията за актуализиране или изтриване. Други таблици всъщо изтриване или актуализиране, включително допълнителни препратки към целевата таблица, продължете да използвате версиите на редове .

Вероятно са необходими някои примери, за да направят тези объркващи поведения малко по-ясни...

Настройка за тестване

Следният скрипт гарантира, че всички сме настроени да използваме RCSI, създава проста таблица и добавя два примерни реда към нея:

ALTER DATABASE Sandpit
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
GO
CREATE TABLE dbo.Test
(
    RowID integer PRIMARY KEY,
    Data integer NOT NULL
);
GO
INSERT dbo.Test
    (RowID, Data)
VALUES 
    (1, 1234),
    (2, 2345);

Следващата стъпка трябва да се изпълни в отделна сесия . Той стартира транзакция и изтрива двата реда от тестовата таблица (изглежда странно, но всичко това ще има смисъл скоро):

BEGIN TRANSACTION;
DELETE dbo.Test 
WHERE RowID IN (1, 2);

Имайте предвид, че транзакцията е умишлено оставена отворена . Това поддържа изключителни заключвания и на двата реда, които се изтриват (заедно с обичайните изключващи намерения заключвания на съдържащата страница и самата таблица), като заявката по-долу може да се използва за показване:

SELECT
    resource_type,
    resource_description,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE 
    request_session_id = @@SPID;

Изборният тест

Превключване обратно към оригиналната сесия , първото нещо, което искам да покажа, е, че обикновените оператори за избор, използващи RCSI, все още виждат как двата реда се изтриват. Заявката за избор по-долу използва версии на редове, за да върне най-новите ангажирани данни към момента на започване на изявлението:

SELECT *
FROM dbo.Test;

В случай, че това изглежда изненадващо, не забравяйте, че показването на редовете като изтрити би означавало показване на незаети изглед на данните, което не е разрешено при изолиране на четене.

Тестът за изтриване

Въпреки успеха на теста за избор, опит за изтриване същите тези редове от текущата сесия ще бъдат блокирани. Може да си представите, че това блокиране възниква, когато операцията се опитва да придобие изключително ключалки, но това не е така.

Изтриването не използва версия на редове за да намерите редовете за изтриване; вместо това се опитва да придобие заключвания за актуализация. Заключването на актуализацията е несъвместимо с изключителните заключвания на редове, държани от сесията с отворената транзакция, така че заявката блокира:

DELETE dbo.Test 
WHERE RowID IN (1, 2);

Прогнозният план за заявка за това изявление показва, че редовете, които трябва да бъдат изтрити, се идентифицират чрез редовна операция за търсене, преди отделен оператор да извърши действителното изтриване:

Можем да видим заключванията, задържани на този етап, като изпълним същата заключваща заявка, както преди (от друга сесия), като не забравяме да променим SPID препратката към тази, използвана от блокираната заявка. Резултатите изглеждат така:

Нашата заявка за изтриване е блокирана в оператора Clustered Index Seek, който чака да получи заключване на актуализация за четене данни. Това показва, че намирането на редовете за изтриване под RCSI придобива заключвания за актуализиране, вместо да чете потенциално остарели данни с версии. Също така показва, че блокирането не се дължи на частта за изтриване на операцията, която чака за придобиване на изключително заключване.

Тестът за актуализиране

Отменете блокираната заявка и вместо това опитайте следната актуализация:

UPDATE dbo.Test
SET Data = Data + 1000
WHERE RowID IN (1, 2);

Прогнозният план за изпълнение е подобен на този, който се вижда в теста за изтриване:

Скаларът за изчисляване е там, за да определи резултата от добавяне на 1000 към текущата стойност на колоната с данни във всеки ред, която се чете от Clustered Index Seek. Това изявление също ще блокира когато се изпълни, поради заключването на актуализацията, поискано от операцията за четене. Екранната снимка по-долу показва заключванията, задържани, когато заявката блокира:

Както и преди, заявката се блокира при търсене, като се изчаква несъвместимото изключително заключване да бъде освободено, за да може да бъде придобито заключване за актуализация.

Тестът за вмъкване

Следващият тест включва изявление, което вмъква нов ред в нашата тестова таблица, използвайки стойността на колоната за данни от съществуващия ред с ID 1 в таблицата. Припомнете си, че този ред все още е изключително заключен от сесия с отворената транзакция:

INSERT dbo.Test
    (RowID, Data)
SELECT 3, Data
FROM dbo.Test
WHERE RowID = 1;

Планът за изпълнение отново е подобен на предишните тестове:

Този път заявката не е блокирана . Това показва, че при четене не са получени заключвания за актуализация данни за вложката. Вместо това тази заявка използва версия на реда, за да получи стойността на колоната с данни за нововмъкнатия ред. Заключването на актуализацията не беше получено, защото това изявление не намери редове за промяна , той просто чете данни за използване във вмъкването.

Можем да видим този нов ред в таблицата с помощта на тестовата заявка за избор от преди:

Имайте предвид, че ниесе може да актуализира и изтрие новия ред (което ще изисква заключвания за актуализиране), тъй като няма конфликтно изключително заключване. Сесията с отворената транзакция има изключителни заключвания само на редове 1 и 2:

-- Update the new row
UPDATE dbo.Test
SET Data = 9999
WHERE RowID = 3;
-- Show the data
SELECT * FROM dbo.Test;
-- Delete the new row
DELETE dbo.Test
WHERE RowID = 3;

Този тест потвърждава, че инструкциите за вмъкване не придобиват заключвания за актуализиране при четене , защото за разлика от актуализациите и изтриванията те не променят съществуващ ред. Частта за четене на вложка изразът използва нормалното поведение на версията на RCSI редове.

Многократен референтен тест

По-рано споменах, че само препратката към единична таблица, използвана за намиране на редове за модифициране, придобива заключвания за актуализиране; други таблици в същия оператор за актуализиране или изтриване все още четат версии на редове. Като специален случай на този общ принцип, изявление за промяна на данни с множество препратки към същата таблица прилага само заключвания за актуализиране на един екземпляр използва се за намиране на редове за промяна. Този последен тест илюстрира това по-сложно поведение, стъпка по стъпка.

Първото нещо, което ще ни трябва, е нов трети ред за нашата тестова таблица, този път с нула в колоната за данни:

INSERT dbo.Test
    (RowID, Data)
VALUES
    (3, 0);

Както се очаква, това вмъкване продължава без блокиране, което води до таблица, която изглежда така:

Не забравяйте, че втората сесия все още е изключителна заключва на редове 1 и 2 в този момент. Ние сме свободни да придобием ключалки на ред 3, ако трябва. Следната заявка е тази, която ще използваме, за да покажем поведението с множество препратки към целевата таблица:

-- Multi-reference update test
UPDATE WriteRef
SET Data = ReadRef.Data * 2
OUTPUT 
    ReadRef.RowID, 
    ReadRef.Data,
    INSERTED.RowID AS UpdatedRowID,
    INSERTED.Data AS NewDataValue
FROM dbo.Test AS ReadRef
JOIN dbo.Test AS WriteRef
    ON WriteRef.RowID = ReadRef.RowID + 2
WHERE 
    ReadRef.RowID = 1;

Това е по-сложна заявка, но нейната работа е сравнително проста. Има две препратки към тестовата таблица, едната имам псевдоним ReadRef, а другата WriteRef. Идеята е дачете от ред 1 (с помощта на версия на ред) чрез ReadRef и за актуализация третият ред (който ще се нуждае от заключване за актуализиране) с помощта на WriteRef.

Заявката указва изрично ред 1 в клаузата where за препратка към таблицата за четене. Той се присъединява към препратката за писане към същата таблица чрез добавяне на 2 към този идентификатор на ред (така че идентифицира ред 3). Инструкцията за актуализиране също използва изходна клауза, за да върне набор от резултати, показващ стойностите, прочетени от изходната таблица и произтичащите промени, направени в ред 3.

Приблизителният план за заявка за това изявление е както следва:

Свойствата на търсенето с етикет (1) показват, че това търсене е в ReadRef псевдоним, четене на данни от реда с RowID 1:

Тази операция за търсене не намира ред, който ще бъде актуализиран, така че заключванията за актуализиране не взета; четенето се извършва с помощта на версионни данни. Четенето не се блокира от изключителните заключвания, държани от другата сесия.

Изчислителният скалар с етикет (2) дефинира израз с етикет 1004, който изчислява актуализираната стойност на колоната с данни. Израз 1009 изчислява идентификатора на ред, който трябва да бъде актуализиран (1 + 2 =идентификатор на ред 3):

Второто търсене е препратка към същата таблица (3). Това търсене намира реда, който ще бъде актуализиран (ред 3) с помощта на израз 1009:

Тъй като това търсене намира ред, който трябва да бъде променен, заключване за актуализиране се взема вместо да се използват версии на редове. Няма конфликтно изключително заключване на ред ID 3, така че заявката за заключване се предоставя незабавно.

Крайният маркиран оператор (4) е самата операция за актуализиране. Заключването на актуализацията на ред 3 е надстроено до изключително заключете в този момент, точно преди промяната да бъде извършена. Този оператор също връща данните, посочени в изходната клауза на изявлението за актуализиране:

Резултатът от оператора за актуализиране (генериран от изходната клауза) е показан по-долу:

Крайното състояние на таблицата е както е показано по-долу:

Можем да потвърдим заключванията, взети по време на изпълнение, като използваме проследяване на Profiler:

Това показва, че само една актуализация заключване на ред ключ е придобито. Когато този ред достигне до оператора за актуализиране, заключването се преобразува в изключително ключалка. В края на изявлението заключването се освобождава.

Може да успеете да видите от изхода за проследяване, че хеш стойността на заключване за заключения ред с актуализация е (98ec012aa510) в моята тестова база данни. Следната заявка показва, че този хеш за заключване наистина е свързан с RowID 3 в клъстерирания индекс:

SELECT RowID, %%LockRes%%
FROM dbo.Test;

Имайте предвид, че заключванията за актуализация, взети в тези примери, са по-краткотрайни от заключванията за актуализиране, взети, ако посочим UPDLOCK намек. Тези вътрешни заключвания за актуализиране се освобождават в края на оператора, докато UPDLOCK заключванията се задържат до края на транзакцията.

Това приключва демонстрацията на случаите, в които RCSI придобива заключвания за актуализация, за да чете текущи записани данни, вместо да използва версия на редове.

Споделени и ключови заключване под RCSI

Има редица други сценарии, при които машината на базата данни все още може да придобие ключалки под RCSI. Всички тези ситуации са свързани с необходимостта от запазване на коректността, която би била застрашена от разчитането на потенциално остарели данни с версии.

Споделените заключвания са взети за проверка на чужд ключ

За две таблици в директна връзка с външен ключ, машината на базата данни трябва да предприеме стъпки, за да гарантира, че ограниченията не са нарушени чрез разчитане на потенциално остарели четения с версии. Текущата реализация прави това чрез превключване към заключено четене, извършено при достъп до данни като част от автоматична проверка на външния ключ.

Вземането на споделени ключалки гарантира, че проверката на целостта чете най-новите записани данни (не стара версия) или блокира поради едновременна модификация по време на полет. Превключването към блокиране на четене се прилага само за конкретния метод за достъп, използван за проверка на данните за външния ключ; другият достъп до данни в същия оператор продължава да използва версии на редове.

Това поведение се прилага само за изрази, които променят данни, където промяната пряко засяга връзка с външен ключ. За модификации на реферираната (родителска) таблица това означава актуализации, които засягат реферираната стойност (освен ако не е зададена на NULL ) и всички изтривания. За референтната (дъщерна) таблица това означава всички вмъквания и актуализации (отново, освен ако препратката на ключа не е NULL ). Същите съображения важат за компонентните ефекти на MERGE .

Примерен план за изпълнение, показващ търсене на външен ключ, който приема споделени брави, е показан по-долу:

Може да се сериализира за каскадни външни ключове

Когато връзката с външния ключ има каскадно действие, коректността изисква локална ескалация до сериализираща семантика на изолация. Това означава, че ще видите заключванията на диапазона на ключове, взети за каскадно референтно действие. Както беше случаят с заключванията за актуализация, видяни по-рано, тези заключвания на диапазона на ключове са обхванати за израза, а не за транзакцията. Примерен план за изпълнение, показващ къде се вземат вътрешните сериализиращи се ключалки под RCSI, е показан по-долу:

Други сценарии

Има много други специфични случаи, при които двигателят автоматично удължава живота на ключалките или локално повишава нивото на изолация, за да гарантира коректност. Те включват сериализиращата се семантика, използвана при поддържане на свързан индексиран изглед или при поддържане на индекс, който има IGNORE_DUP_KEY опцията е зададена.

Съобщението е, че RCSI намалява количеството заключване, но не винаги може да го елиминира напълно.

Следващия път

Следващата публикация от тази серия разглежда нивото на изолация на моментна снимка.

[ Вижте индекса за цялата серия ]


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Присъединете се към нас в Лас Вегас за SQLintersection и спестете $100

  2. Анализирайте големи данни с Microsoft Azure Tools

  3. Архивиране на SQL бази данни с VDP Advanced SQL Agent

  4. Открийте как кардиналността влияе на производителността

  5. Свързване на SAS JMP към Salesforce.com