Чи є одна заява SQL Server атомарною та послідовною?


76

Чи є оператором SQL Server ACID?

Що я маю на увазі під цим

Враховуючи єдиний оператор T-SQL, не загорнутий у BEGIN TRANSACTION/ COMMIT TRANSACTION, виконуються дії цього оператора:

  • Атомний : або виконуються всі модифікації його даних, або не виконується жодна з них.
  • Послідовність : Після завершення транзакція повинна залишити всі дані у стабільному стані.
  • Ізольовані : Зміни, внесені паралельними транзакціями, повинні бути ізольованими від модифікацій, внесені будь-якими іншими паралельними транзакціями.
  • Довговічний : Після завершення транзакції її наслідки постійно зберігаються в системі.

Причину, про яку я запитую

У мене є єдине твердження в реальній системі, яке, здається, порушує правила запиту.

По суті, моє твердження T-SQL:

--If there are any slots available, 
--then find the earliest unbooked transaction and mark it booked
UPDATE Transactions
SET Booked = 1
WHERE TransactionID = (
   SELECT TOP 1 TransactionID
   FROM Slots
      INNER JOIN Transactions t2
      ON Slots.SlotDate = t2.TransactionDate
   WHERE t2.Booked = 0 --only book it if it's currently unbooked
   AND Slots.Available > 0 --only book it if there's empty slots
   ORDER BY t2.CreatedDate)

Примітка : Але більш простим концептуальним варіантом може бути:

--Give away one gift, as long as we haven't given away five
UPDATE Gifts
SET GivenAway = 1
WHERE GiftID = (
   SELECT TOP 1 GiftID
   FROM Gifts
   WHERE g2.GivenAway = 0
   AND (SELECT COUNT(*) FROM Gifts g2 WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)

В обох цих твердженнях зауважте, що це окремі твердження ( UPDATE...SET...WHERE).

Бувають випадки, коли неправильна операція «забронюється» ; це фактично вибір пізнішої транзакції. Подивившись на це протягом 16 годин, я запнута. Це ніби SQL Server просто порушує правила.

Мені цікаво, а що, якщо результати Slotsподання зміниться до того, як відбудеться оновлення? Що робити, якщо SQL Server не SHAREDзамикає транзакції на цю дату ? Чи можливо, що одне твердження може бути суперечливим?

Тому я вирішив це протестувати

Я вирішив перевірити, чи результати підзапитів або внутрішніх операцій несумісні. Я створив просту таблицю з одним intстовпцем:

CREATE TABLE CountingNumbers (
   Value int PRIMARY KEY NOT NULL
)

З кількох з'єднань, у щільному циклі, я викликаю єдиний оператор T-SQL :

INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers

Іншими словами, псевдокодом є:

while (true)
{
    ADOConnection.Execute(sql);
}

І за кілька секунд я отримую:

Violation of PRIMARY KEY constraint 'PK__Counting__07D9BBC343D61337'. 
Cannot insert duplicate key in object 'dbo.CountingNumbers'. 
The duplicate value is (1332)

Чи є атомарними твердження?

Той факт, що одне твердження не було атомним, змушує мене задуматися, чи є окремі твердження атомними?

Або існує більш тонка дефініція висловлювання , яка відрізняється від (наприклад) того, що SQL Server вважає заявою:

введіть тут опис зображення

Чи це принципово означає, що в межах одного оператора T-SQL оператори SQL Server не є атомними?

І якщо одне твердження є атомним, що пояснює ключове порушення?

Зсередини збереженої процедури

Замість того, щоб віддалений клієнт відкривав n підключень, я спробував це за допомогою збереженої процедури:

CREATE procedure [dbo].[DoCountNumbers] AS

SET NOCOUNT ON;

DECLARE @bumpedCount int
SET @bumpedCount = 0

WHILE (@bumpedCount < 500) --safety valve
BEGIN
SET @bumpedCount = @bumpedCount+1;

PRINT 'Running bump '+CAST(@bumpedCount AS varchar(50))

INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers

IF (@bumpedCount >= 500)
BEGIN
    PRINT 'WARNING: Bumping safety limit of 500 bumps reached'
END
END

PRINT 'Done bumping process'

і відкрив 5 вкладок у SSMS, натиснув F5 у кожній і спостерігав, як вони теж порушують кислоту:

Running bump 414
Msg 2627, Level 14, State 1, Procedure DoCountNumbers, Line 14
Violation of PRIMARY KEY constraint 'PK_CountingNumbers'. 
Cannot insert duplicate key in object 'dbo.CountingNumbers'. 
The duplicate key value is (4414).
The statement has been terminated.

Отже, помилка не залежить від ADO, ADO.net або жодного з перерахованих вище.

Протягом 15 років я працюю, припускаючи, що один вираз у SQL Server є послідовним; і єдиний

А як щодо РІВНЯ ІЗОЛЯЦІЇ ОПЕРАЦІЇ xxx?

Для різних варіантів пакета SQL для виконання:

  • за замовчуванням (прочитано) : порушення ключа

    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    
  • за замовчуванням (читання здійснено), явна транзакція : не порушено ключ помилки

    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    
  • серіалізується : тупик

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    
  • знімок (після зміни бази даних, щоб увімкнути ізоляцію знімка): порушення ключа

    SET TRANSACTION ISOLATION LEVEL SNAPSHOT
    BEGIN TRANSACTION
    INSERT INTO CountingNumbers (Value)
    SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
    COMMIT TRANSACTION
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    

Бонус

  • Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64)
  • Рівень ізоляції транзакцій за замовчуванням ( READ COMMITTED)

Виявляється, кожен запит, який я коли-небудь писав, порушений

Це, безумовно, змінює ситуацію. Кожне твердження про оновлення, яке я коли-небудь писав, принципово порушено. Наприклад:

--Update the user with their last invoice date
UPDATE Users 
SET LastInvoiceDate = (SELECT MAX(InvoiceDate) FROM Invoices WHERE Invoices.uid = Users.uid)

Неправильне значення; оскільки інший рахунок може бути вставлений після MAXі перед UPDATE. Або приклад з BOL:

UPDATE Sales.SalesPerson
SET SalesYTD = SalesYTD + 
    (SELECT SUM(so.SubTotal) 
     FROM Sales.SalesOrderHeader AS so
     WHERE so.OrderDate = (SELECT MAX(OrderDate)
                           FROM Sales.SalesOrderHeader AS so2
                           WHERE so2.SalesPersonID = so.SalesPersonID)
     AND Sales.SalesPerson.BusinessEntityID = so.SalesPersonID
     GROUP BY so.SalesPersonID);

без ексклюзивних блокувань, SalesYTDце неправильно.

Як я міг щось робити за всі ці роки.


Що саме ви маєте на увазі під назвою "Бувають випадки, коли" бронюється "неправильна транзакція; це фактично вибір наступної транзакції". ?
ypercubeᵀᴹ

Чи можете ви змусити його вийти з ладу під СЕРІАЛІЗОВАНИМ? Було б цікаво побачити фактичний план виконання з успішного оновлення, зрозуміти індекси в таблиці.
Аарон Бертран,

5
INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbersне є безпечним в режимі UNCOMMITTED, COMMITTED, REPEATABLE READ і SERIALIZABLE, оскільки для цього потрібні Sзамки. А Sзамок сумісний з іншим Sзамком. Інші паралельні оператори / транзакції все ще можуть читати ті самі рядки. Щоб бути в безпеці , цей підхід повинен SERIALIZABLE рівня ізоляції плюс Xзамки або Xзамки + HOLDLOCK табличних підказок: INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers WITH(XLOCK, HOLDLOCK).
Богдан Сахлеан

3
Здається, ви робите поширену помилку, плутаючи атомну та ізоляційну. Atomic просто означає успіх або невдачу як одиниці (транзакція, що здійснюється, або всі відкочуються). Це нічого не говорить про видимість змін від одночасних транзакцій.
Martin Smith

3
@BogdanSahlean - Ні, це стосується назви запитання. Ні атомність, ні послідовність не обіцяють того, що, здається, передбачається. Атомний: Просто означає успіх або невдачу як одиниці та послідовний: жодних обмежень тощо не порушено.
Martin Smith

Відповіді:


23

Я працював, припускаючи, що одне твердження в SQL Server є послідовним

Це припущення хибне. Наступні дві транзакції мають однакову семантику блокування:

STATEMENT

BEGIN TRAN; STATEMENT; COMMIT

Ніякої різниці взагалі. Окремі оператори та автоматичні фіксації нічого не змінюють.

Тож об’єднання всієї логіки в одне твердження не допомагає (якщо це так, то це було випадково, оскільки план змінився).

Давайте вирішимо проблему. SERIALIZABLEвиправить невідповідність, яку ви бачите, оскільки це гарантує, що ваші транзакції поводяться так, ніби вони виконуються однопотоково. Як і рівноцінно, вони поводяться так, ніби їх страчують миттєво.

Ви отримаєте тупикові ситуації. Якщо у вас все гаразд із повторною спробою, ви на цьому закінчили.

Якщо ви хочете витратити більше часу, застосуйте підказки щодо блокування, щоб змусити ексклюзивний доступ до відповідних даних:

UPDATE Gifts  -- U-locked anyway
SET GivenAway = 1
WHERE GiftID = (
   SELECT TOP 1 GiftID
   FROM Gifts WITH (UPDLOCK, HOLDLOCK) --this normally just S-locks.
   WHERE g2.GivenAway = 0
    AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)

Тепер ви побачите зменшений паралелізм. Це може бути цілком нормально, залежно від вашого навантаження.

Сама природа вашої проблеми ускладнює досягнення паралелізму. Якщо вам потрібне рішення для цього, нам доведеться застосовувати більш інвазивні методи.

Ви можете трохи спростити ОНОВЛЕННЯ:

WITH g AS (
   SELECT TOP 1 Gifts.*
   FROM Gifts
   WHERE g2.GivenAway = 0
    AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
   ORDER BY g2.GiftValue DESC
)
UPDATE g  -- U-locked anyway
SET GivenAway = 1

Це позбавляє від одного непотрібного об’єднання.


2
Це дещо жахливо дізнатися. Я роками писав окремі оператори SQL, припускаючи, що кінцевий результат заяви відповідає тому, про що я просив. Також зауважте, що жоден із запитів у тому, що є справжня система (це додало б великої складності питанню). Концепція, яку я досліджую і розірвавши весь мір світогляду, полягає в тому, чи є твердження послідовним у собі. (Я завжди знав, що для декількох операторів в рамках однієї партії або декількох пакетів потрібні явні транзакції).
Ян Бойд

@IanBoyd, отже, ти не вважав, що рівень ізоляції відіграє у цьому певну роль?
ypercubeᵀᴹ

@ypercube я розглядав роль рівня ізоляції; але я вважав лише ПРОЧИТАНО ВЧИНЕНО.
Ian Boyd

Що ж, зараз весь мій світ зруйнований. Кожне твердження UPDATE або DELETE, яке я коли-небудь писав, є принципово неправильним. Я припускав, що можу запитувати значення, і щоб ці значення були правильними протягом тривалості оператора.
Ian Boyd

2
@IanBoyd правда, тепер ти повинен перевірити все, що є важливим. З іншого боку, я не вважаю, що одночасність баз даних дуже турбує на практиці. З якихось причин більшість запитів не є проблематичними. Можливо, тому, що вони холодні, і я можу просто зробити їх СЕРІАЛІЗОВАНИМИ. Іноді ви можете використовувати ізоляцію SNAPSHOT, щоб отримати послідовний вигляд бази даних. Іноді можна використовувати просту ієрархію блокування (приклад: коли транзакція хоче змінити дані клієнта (можливо, додати замовлення), вони спочатку блокують відповідний рядок Клієнтів. Це усуває проблеми паралельності для окремих клієнтів.).
usr

3

Нижче наведено приклад оператора UPDATE, який атомно збільшує значення лічильника

-- Do this once for test setup
CREATE TABLE CountingNumbers (Value int PRIMARY KEY NOT NULL)
INSERT INTO CountingNumbers VALUES(1) 

-- Run this in parallel: start it in two tabs on SQL Server Management Studio
-- You will see each connection generating new numbers without duplicates and without timeouts
while (1=1)
BEGIN
  declare @nextNumber int
  -- Taking the Update lock is only relevant in case this statement is part of a larger transaction
  -- to prevent deadlock
  -- When executing without a transaction, the statement will itself be atomic
  UPDATE CountingNumbers WITH (UPDLOCK, ROWLOCK) SET @nextNumber=Value=Value+1
  print @nextNumber
END
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.