Является ли отдельный оператор SQL Server атомарным и последовательным?
Это заявление в 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 в каждой и наблюдал, как они тоже нарушили ACID:
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
неправильно.
Как я смог сделать что-нибудь все эти годы.
3 ответа
Я действовал в предположении, что один оператор в 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
Это избавляет от одного ненужного соединения.
Ниже приведен пример оператора 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
Select не блокируется исключительно, даже сериализуемый блокирует, а только на время выполнения select!По окончании выбора блокировка выбора снимается. Затем вступают в действие блокировки обновления, поскольку теперь они знают, что блокировать, поскольку Select возвращает результаты. Между тем, любой другой может снова выбрать!
Единственный надежный способ безопасно прочитать и заблокировать строку:
begin transaction
--lock what i need to read
update mytable set col1=col1 where mykey=@key
--now read what i need
select @d1=col1,@d2=col2 from mytable where mykey=@key
--now do here calculations checks whatever i need from the row i read to decide my update
if @d1<@d2 set @d1=@d2 else set @d1=@d2 * 2 --just an example calc
--now do the actual update on what i read and the logic
update mytable set col1=@d1,col2=@d2 where mykey=@key
commit transaction
Таким образом, любое другое соединение, выполняющее тот же оператор для тех же данных, обязательно будет ждать первого (фальшивого) оператора обновления, пока не будет выполнено предыдущее. Это гарантирует, что при снятии блокировки только одному соединению будет предоставлено разрешение на блокировку запроса на «обновление», и это одно обязательно будет читать зафиксированные завершенные данные, чтобы производить вычисления и решать, нужно ли и что на самом деле обновлять при втором «реальном» обновлении.
Другими словами, когда вам нужно выбрать информацию, чтобы решить, нужно ли / как обновлять, вам нужен блок транзакции начала / фиксации, плюс вам нужно начать с поддельного обновления того, что вам нужно выбрать, - прежде чем вы его выберете (вывод обновления будет тоже делаю).