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

 李林1108_965 发布于 2023-01-30 11:58

是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)
陈述是原子的吗?

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

或者是否有一个更微妙语句定义,它与(例如)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中的单个语句是一致的情况下运行; 而唯一的

TRANSACTION ISOLATION LEVEL xxx怎么样?

对于要执行的SQL批处理的不同变体:

default(读已提交):密钥违规

INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers

default(读提交),显式事务:没有错误密钥违规

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是错误的.

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

1 个回答
  • 我一直在假设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
    

    这摆脱了一个不必要的连接.

    2023-01-30 12:01 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有