Является ли выражение в SQL Server ACID
?
Что я подразумеваю под этим
Учитывая один оператор T-SQL, не заключенный в BEGIN TRANSACTION
/COMMIT TRANSACTION
, являются действиями этого оператора:
- Atomic: либо выполняются все его модификации данных, либо ни один из них не выполняется.
- Согласовано. По завершении транзакция должна оставить все данные в согласованном состоянии.
- Изолированные. Модификации, выполняемые параллельными транзакциями, должны быть изолированы от изменений, сделанных любыми другими параллельными транзакциями.
- Прочный: после завершения транзакции его эффекты постоянно установлены в системе.
Причина, по которой я прошу
У меня есть один оператор в живой системе, который, по-видимому, нарушает правила запроса.
Фактически мой оператор 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 currently unbooked
AND Slots.Available > 0 --only book it if there 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
Bonus
- 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);
без эксклюзивных holdlocks, SalesYTD
неверно.
Как я мог делать все эти годы.