SQLServerACID
中是否有语句?
给定一个没有包装在 BEGIN TRANSACTION
/COMMIT TRANSACTION
中的 T-SQL 语句,就是该语句的操作:
在一个活动系统中,我有一条似乎违反查询规则的语句。
实际上,我的 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个小时,我被难住了。似乎 SQLServer 只是违反了规则。
我想知道如果在更新发生之前 Slots
视图的结果发生了变化会怎么样?如果 SQLServer 没有在该 约会上的 交易上持有 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)
单个语句不是原子的事实让我怀疑单个语句是否是原子的?
或者 声明还有更多的 真狡猾定义,它不同于(例如) SQLServer 认为的语句:
这是否从根本上意味着在单个 T-SQL 语句的范围内,SQLServer 语句不是原子的?
如果单个语句是原子的,那么如何解释键冲突呢?
我没有使用远程客户端打开 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'
在 SSMS 中打开5个标签,按下每个标签中的 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年来,我一直假设 SQLServer 中的一条语句是一致的;
对于要执行的 SQL 批处理的不同变体:
默认值(读提交) : 键违规
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
default (read committed), explicit transaction: no error key violation
BEGIN TRANSACTION
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
COMMIT TRANSACTION
serializable: deadlock
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
snapshot (after altering database to enable snapshot isolation): key violation
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
READ COMMITTED
)This certainly changes things. Every update statement I've ever written is fundamentally broken. E.g.:
--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
就是错误的。
这么多年来我是怎么做到的。