单个SQL Server语句是原子的还是一致的?

时间:2014-01-30 22:26:01

标签: sql-server sql-server-2008-r2

是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'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认为的语句不同:

enter image description here

这是否从根本上意味着在单个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'

并在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年来,我一直在假设SQL Server中的单个语句是一致的情况下运行;而唯一的

TRANSACTION ISOLATION LEVEL 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

结果我写过的每个查询都被破坏了

这肯定会改变一切。我写过的每一个更新声明都从根本上被打破了。 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是错误的。

这些年来我怎么能做任何事情。

3 个答案:

答案 0 :(得分:16)

  

我一直在假设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
)

您现在将看到减少的并发性。根据您的负载,这可能完全没问题。

您的问题的本质使得难以实现并发性。如果您需要解决方案,我们需要采用更具侵入性的技术。

您可以稍微简化UPDATE:

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

这消除了一个不必要的连接。

答案 1 :(得分:2)

以下是以原子方式递增计数器值的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

答案 2 :(得分:0)

Select 不会独占锁定,即使可序列化也可以,但仅限于执行 select 的时间! 一旦 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

这样,对于相同数据运行相同语句的任何其他连接肯定会在第一个(假)更新语句处等待,直到前一个完成。这确保了当锁定被释放时,只有一个连接将授予“更新”锁定请求的权限,并且这个连接肯定会读取提交的最终数据以进行计算并决定是否在第二个“真正”更新时实际更新以及实际更新什么。

换句话说,当您需要选择信息来决定是否/如何更新时,您需要一个开始/提交事务块,并且您需要从您需要选择的内容的虚假更新开始 - 在您选择它之前(更新输出也可以)。