AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / dba / 问题 / 58042
Accepted
Ian Boyd
Ian Boyd
Asked: 2014-02-01 18:50:39 +0800 CST2014-02-01 18:50:39 +0800 CST 2014-02-01 18:50:39 +0800 CST

SQL Server 在 UPDATE 期间如何同时返回新值和旧值?

  • 772

在高并发期间,我们遇到了查询返回无意义结果的问题——结果违反了发出查询的逻辑。重现该问题需要一段时间。我已经设法将可重现的问题提炼为少量的 T-SQL。

注意:有问题的实时系统部分由 5 个表、4 个触发器、2 个存储过程和 2 个视图组成。我已将真实系统简化为更易于管理的已发布问题。事情已经减少,列被删除,存储过程被内联,视图变成了公用表表达式,列的值发生了变化。这就是说,虽然下面的内容重现了错误,但它可能更难以理解。您必须避免想知道为什么某些东西是这样构造的。我在这里试图弄清楚为什么错误情况会在这个玩具模型中重复发生。

/*
The idea in this system is that people are able to take days off. 
We create a table to hold these *"allocations"*, 
and declare sample data that only **1** production operator 
is allowed to take time off:
*/
IF OBJECT_ID('Allocations') IS NOT NULL DROP TABLE Allocations
CREATE TABLE [dbo].[Allocations](
    JobName varchar(50) PRIMARY KEY NOT NULL,
    Available int NOT NULL
)
--Sample allocation; there is 1 avaialable slot for this job
INSERT INTO Allocations(JobName, Available)
VALUES ('Production Operator', 1);

/*
Then we open up the system to the world, and everyone puts in for time. 
We store these requests for time off as *"transactions"*. 
Two production operators requested time off. 
We create sample data, and note that one of the users 
created their transaction first (by earlier CreatedDate):
*/
IF OBJECT_ID('Transactions') IS NOT NULL DROP TABLE Transactions;
CREATE TABLE [dbo].[Transactions](
    TransactionID int NOT NULL PRIMARY KEY CLUSTERED,
    JobName varchar(50) NOT NULL,
    ApprovalStatus varchar(50) NOT NULL,
    CreatedDate datetime NOT NULL
)
--Two sample transactions
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (52625, 'Production Operator', 'Booked', '20140125 12:00:40.820');
INSERT INTO Transactions (TransactionID, JobName, ApprovalStatus, CreatedDate)
VALUES (60981, 'Production Operator', 'WaitingList', '20150125 12:19:44.717');

/*
The allocation, and two sample transactions are now in the database:
*/
--Show the sample data
SELECT * FROM Allocations
SELECT * FROM Transactions

交易都插入为WaitingList. 接下来,我们有一个周期性任务运行,寻找空槽并将 WaitingList 上的任何人撞到 Booked 状态。

在一个单独的 SSMS 窗口中,我们有模拟的循环存储过程:

/*
    Simulate recurring task that looks for empty slots, 
    and bumps someone on the waiting list into that slot.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

--DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 1000000)
BEGIN
    SET @attempts = @attempts+1;

    /*
        The concept is that if someone is already "Booked", then they occupy an available slot.
        We compare the configured amount of allocations (e.g. 1) to how many slots are used.
        If there are any slots leftover, then find the **earliest** created transaction that 
        is currently on the WaitingList, and set them to Booked.
    */

    PRINT '=== Looking for someone to bump ==='
    WITH AvailableAllocations AS (
        SELECT 
            a.JobName,
            a.Available AS Allocations, 
            ISNULL(Booked.BookedCount, 0) AS BookedCount, 
            a.Available-ISNULL(Booked.BookedCount, 0) AS Available
        FROM Allocations a
            FULL OUTER JOIN (
                SELECT t.JobName, COUNT(*) AS BookedCount
                FROM Transactions t
                WHERE t.ApprovalStatus IN ('Booked') 
                GROUP BY t.JobName
            ) Booked
            ON a.JobName = Booked.JobName
        WHERE a.Available > 0
    )
    UPDATE Transactions SET ApprovalStatus = 'Booked'
    WHERE TransactionID = (
        SELECT TOP 1 t.TransactionID
        FROM AvailableAllocations aa
            INNER JOIN Transactions t
            ON aa.JobName = t.JobName
            AND t.ApprovalStatus = 'WaitingList'
        WHERE aa.Available > 0
        ORDER BY t.CreatedDate 
    )


    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

最后在第三个 SSMS 连接窗口中运行它。这模拟了一个并发问题,即早期事务从占用一个槽到等待列表:

/*
    Toggle the earlier transaction back to "WaitingList".
    This means there are two possibilies:
       a) the transaction is "Booked", meaning no slots are available. 
          Therefore nobody should get bumped into "Booked"
       b) the transaction is "WaitingList", 
          meaning 1 slot is open and both tranasctions are "WaitingList"
          The earliest transaction should then get "Booked" into the slot.

    There is no time when there is an open slot where the 
    first transaction shouldn't be the one to get it - he got there first.
*/
SET NOCOUNT ON;

--Reset the faulty row so we can continue testing
UPDATE Transactions SET ApprovalStatus = 'WaitingList'
WHERE TransactionID = 60981

DECLARE @attempts int
SET @attempts = 0;

WHILE (@attempts < 100000)
BEGIN
    SET @attempts = @attempts+1

    /*Flip the earlier transaction from Booked back to WaitingList
        Because it's now on the waiting list -> there is a free slot.
        Because there is a free slot -> a transaction can be booked.
        Because this is the earlier transaction -> it should always be chosen to be booked
    */
    --DBCC TRACEON(3604,1200,3916,-1) WITH NO_INFOMSGS

    PRINT '=== Putting the earlier created transaction on the waiting list ==='

    UPDATE Transactions
    SET ApprovalStatus = 'WaitingList'
    WHERE TransactionID = 52625

    --DBCC TRACEOFF(3604,1200,3916,-1) WITH NO_INFOMSGS

    IF EXISTS(SELECT * FROM Transactions WHERE TransactionID = 60981 AND ApprovalStatus = 'Booked')
    begin
        RAISERROR('The later tranasction, that should never be booked, managed to get booked!', 16, 1)
        BREAK;
    END
END

从概念上讲,碰撞过程一直在寻找任何空槽。如果它找到一个,它将获取最早的事务WaitingList并将其标记为Booked。

在没有并发的情况下进行测试时,逻辑有效。我们有两个交易:

  • 12:00 pm:等候名单
  • 12:20 pm:等候名单

有 1 个分配和 0 个已预订交易,因此我们将较早的交易标记为已预订:

  • 12:00 pm:预订
  • 12:20 pm:等候名单

下次任务运行时,现在有 1 个插槽被占用 - 所以没有什么可更新的。

如果我们然后更新第一个事务,并将其放入WaitingList:

UPDATE Transactions SET ApprovalStatus='WaitingList'
WHERE TransactionID = 60981

然后我们回到我们开始的地方:

  • 12:00 pm:等候名单
  • 12:20 pm:等候名单

注意:您可能想知道为什么我将交易放回等候名单。这是简化玩具模型的牺牲品。在实际系统中事务可以PendingApproval,其中也占用一个槽。PendingApproval 交易在被批准后被放入等待列表。没关系。别担心。

但是当我引入并发性时,通过第二个窗口不断地将第一笔交易在被预订后放回等待列表中,那么后来的交易成功地获得了预订:

  • 12:00 pm:等候名单
  • 12:20 pm:预订

玩具测试脚本捕捉到这一点,并停止迭代:

Msg 50000, Level 16, State 1, Line 41
The later tranasction, that should never be booked, managed to get booked!

为什么?

问题是,为什么在这个玩具模型中会触发这种救助条件?

第一笔交易的审批状态有两种可能的状态:

  • Booked:这种情况下slot被占用,后面的事务不能拥有它
  • WaitingList:在这种情况下,有一个空槽和两个需要它的事务。但由于我们总是select最旧的交易(即ORDER BY CreatedDate)第一个交易应该得到它。

我想可能是因为其他索引

我了解到,在 UPDATE 开始并修改数据后,可以读取旧值。在初始条件下:

  • 聚集索引:Booked
  • 非聚集索引:Booked

然后我进行更新,虽然聚集索引叶节点已被修改,但任何非聚集索引仍然包含原始值并且仍然可以读取:

  • 聚集索引(排他锁):Booked WaitingList
  • 非聚集索引:(解锁)Booked

但这并不能解释观察到的问题。是的,交易不再是Booked,这意味着现在有一个空槽。但是该更改尚未提交,它仍然被排他地持有。如果碰撞程序运行,它会:

  • block:如果快照隔离数据库选项关闭
  • 读取旧值(例如Booked):如果快照隔离开启

无论哪种方式,碰撞工作都不会知道有一个空槽。

所以我不知道

几天来,我们一直在努力弄清楚这些荒谬的结果是如何发生的。

您可能不了解原始系统,但有一套玩具可重现的脚本。当检测到无效案件时,他们会采取救助措施。为什么会被检测到?为什么会这样?

奖金问题

纳斯达克如何解决这个问题?cavirtex 怎么样?mtgox 怎么样?

tl;博士

共有三个脚本块。将它们放入 3 个单独的 SSMS 选项卡并运行它们。第二个和第三个脚本将引发错误。帮我弄清楚为什么会出现错误。

sql-server-2008-r2 locking
  • 1 1 个回答
  • 5004 Views

1 个回答

  • Voted
  1. Best Answer
    Paul White
    2014-02-02T00:01:02+08:002014-02-02T00:01:02+08:00

    默认的READ COMMITTED事务隔离级别保证你的事务不会读取未提交的数据。它不保证您读取的任何数据在您再次读取时将保持不变(可重复读取)或不会出现新数据(幻影)。

    这些相同的考虑适用于同一语句中的多个数据访问。

    您的语句生成了一个多次UPDATE访问该表的计划,因此它容易受到不可重复读取和幻像造成的影响。Transactions

    多路访问

    READ COMMITTED该计划有多种方法可以产生您在孤立情况下不期望的结果。

    一个例子

    第一个Transactions表访问查找状态为 的行WaitingList。第二次访问计算状态为 的条目(针对同一作业)的数量Booked。第一次访问可能只返回后面的事务(此时是较早的事务Booked)。当第二次(计数)访问发生时,较早的事务已更改为WaitingList. 因此,后面的行有资格更新到Booked状态。

    解决方案

    有几种方法可以设置隔离语义以获得您想要的结果,主要是使用正确的隔离级别。

    使用READ_COMMITTED_SNAPSHOT不会是一个解决方案。这种读取提交隔离的实现确实提供了语句级读取一致性(其中不可重复读取和幻像是不可能的),但 SQL Server 在定位要更新的行时会使用更新锁。这意味着用于定位要更新的行的访问路径始终读取最新提交的数据。在同一语句中第二次读取同一数据结构将读取版本化行。

    有关详细信息,请参阅我 在 Read Committed Snapshot Isolation 下的文章 Data Modifications。

    隔离级别将SNAPSHOT在事务开始时为所有读取提供一致的快照,但您可能会遇到写入冲突错误。

    其他备注

    我不得不说,我不会以这种方式设计架构或查询。所涉及的工作比满足规定的业务要求所必需的要多。也许这部分是问题简化的结果,无论如何这是一个单独的问题。

    您看到的行为并不代表任何类型的错误。给定请求的隔离语义,脚本会产生正确的结果。像这样的并发效果也不限于多次访问数据的计划。

    读提交隔离级别提供的保证比通常假设的要少得多。例如,跳过行和/或多次读取同一行是完全可能的。

    • 12

相关问题

  • SQL Server 2008 R2 群集的无人参与安装失败并出现错误 - “路径中有非法字符”。

  • 迁移大型数据库

  • 证明在每个查询中不使用(nolock)提示

  • 代理执行的维护计划

  • 随机化表内容并将它们存储回表中

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    连接到 PostgreSQL 服务器:致命:主机没有 pg_hba.conf 条目

    • 12 个回答
  • Marko Smith

    如何让sqlplus的输出出现在一行中?

    • 3 个回答
  • Marko Smith

    选择具有最大日期或最晚日期的日期

    • 3 个回答
  • Marko Smith

    如何列出 PostgreSQL 中的所有模式?

    • 4 个回答
  • Marko Smith

    列出指定表的所有列

    • 5 个回答
  • Marko Smith

    如何在不修改我自己的 tnsnames.ora 的情况下使用 sqlplus 连接到位于另一台主机上的 Oracle 数据库

    • 4 个回答
  • Marko Smith

    你如何mysqldump特定的表?

    • 4 个回答
  • Marko Smith

    使用 psql 列出数据库权限

    • 10 个回答
  • Marko Smith

    如何从 PostgreSQL 中的选择查询中将值插入表中?

    • 4 个回答
  • Marko Smith

    如何使用 psql 列出所有数据库和表?

    • 7 个回答
  • Martin Hope
    Jin 连接到 PostgreSQL 服务器:致命:主机没有 pg_hba.conf 条目 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane 如何列出 PostgreSQL 中的所有模式? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh 为什么事务日志不断增长或空间不足? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland 列出指定表的所有列 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney MySQL 能否合理地对数十亿行执行查询? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx 如何监控大型 .sql 文件的导入进度? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison 你如何mysqldump特定的表? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 对 SQL 查询进行计时? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas 如何从 PostgreSQL 中的选择查询中将值插入表中? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas 如何使用 psql 列出所有数据库和表? 2011-02-18 00:45:49 +0800 CST

热门标签

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve