单个SQL Server语句是否原子且一致?


75

是SQL Server中的语句ACID吗?

我的意思是

给定一个未包装在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个小时,我很沮丧。好像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)

陈述是原子的吗?

单个语句不是原子的事实使我想知道单个语句是否是原子的?

还是存在一个更微妙statement定义,该定义不同于(例如)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'

并在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中的一条语句是一致的;而唯一的

交易隔离级别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ᵀᴹ

您能否在SERIALIZABLE下失败?有兴趣查看成功更新的实际执行计划,了解表上的索引。
2014年

5
INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers在UNCOMMITTED,COM​​MITTED,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)
Bogdan Sahlean 2014年

3
您似乎犯了混淆原子和隔离的常见错误。原子只是意味着一个单元成功或失败(所有事务已提交或全部回滚)。它并没有说明并发事务中更改的可见性。
马丁·史密斯

3
@BogdanSahlean-不,它是指问题标题。原子性或一致性都不能保证似乎是假定的。原子:仅表示整体成功或失败,并且一致:不违反任何限制等。
2014年

Answers:


22

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

这摆脱了一个不必要的联接。


2
这有点令人恐惧。多年来,我一直在编写单个SQL语句,但前提是该语句的最终结果与我要求的一致。还要注意,这两个查询都不是真正的真实系统(这会增加问题的复杂性)。我正在研究的概念是,如果声明本身是一致的,那么我的整个世界观就会崩溃。(我一直知道,同一批或多个批中的多个语句需要显式事务)。
伊恩·博伊德

@IanBoyd,所以您没有考虑过隔离级别在其中起作用吗?
ypercubeᵀᴹ

@ypercube我曾考虑过隔离级别的作用;但我只考虑读已提交。
伊恩·博伊德

好了,我的整个世界都毁了。我曾经写的每个UPDATE或DELETE语句从根本上都是错误的。我假设我可以查询值,并且在声明的持续时间内这些值是正确的。
伊恩·博伊德

2
@IanBoyd是的,您现在必须审核所有重要的内容。另一方面,我发现数据库并发在实践中并不是很麻烦。由于某些原因,大多数查询都没有问题。也许是因为它们很冷,我可以使它们可序列化。有时,您可以使用SNAPSHOT隔离来获取数据库的一致视图。有时,您可以使用简单的锁定层次结构(例如:每当事务想要修改客户数据(可能添加订单)时,他们首先U锁定相应的“客户”行。这消除了单个客户的并发问题。)。
2014年

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.