【问题标题】:Minimum transaction isolation level to avoid "Lost Updates"避免“丢失更新”的最低事务隔离级别
【发布时间】:2012-01-02 08:08:21
【问题描述】:

借助 SQL Server 的事务隔离级别,您可以避免某些不必要的并发问题,例如脏读等。

我现在感兴趣的是丢失的更新 - 事实上,两个交易可以在没有人注意到的情况下覆盖彼此的更新。我看到并听到关于我必须选择至少哪个隔离级别来避免这种情况的相互矛盾的陈述。

Kalen Delaney 在她的“SQL Server Internals”一书中说(第 10 章 - 事务和并发 - 第 592 页):

在未提交的读取隔离中,前面描述的所有行为,除了丢失更新,都是可能的。

另一方面,给我们上课的独立 SQL Server 培训师告诉我们,我们至少需要“可重复读取”以避免丢失更新。

那么谁是对的?为什么??

【问题讨论】:

    标签: sql-server isolation-level transaction-isolation


    【解决方案1】:

    以下引自70-762 Developing SQL Databases (p. 212)

    当两个进程读取相同的内容时,可能会出现另一个潜在问题 行,然后用不同的值更新该数据。这可能会发生 如果一个事务首先将一个值读入一个变量,然后使用 稍后步骤中的更新语句中的变量。当这个更新 执行,另一个事务更新相同的数据。无论哪一个 这些事务首先提交成为丢失更新,因为它 被另一个事务中的更新所取代。你不能使用 隔离级别来改变这种行为,但你可以写一个 专门允许丢失更新的应用程序。

    因此,在这种情况下,似乎没有任何隔离级别可以帮助您,您需要在代码本身中解决问题。例如:

    DROP TABLE IF EXISTS [dbo].[Balance];
    
    CREATE TABLE [dbo].[Balance]
    (
        [BalanceID] TINYINT IDENTITY(1,1)
       ,[Balance] MONEY
       ,CONSTRAINT [PK_Balance] PRIMARY KEY
       (
            [BalanceID]
       )
    );
    
    INSERT INTO [dbo].[Balance] ([Balance])
    VALUES (100);
    
    -- query window 1
    BEGIN TRANSACTION;
    
        DECLARE @CurrentBalance MONEY;
    
        SELECT @CurrentBalance = [Balance]
        FROM [dbo].[Balance]
        WHERE [BalanceID] = 1;
    
        WAITFOR DELAY '00:00:05'
    
        UPDATE [dbo].[Balance]
        SET [Balance] = @CurrentBalance + 20
        WHERE [BalanceID] = 1;
    
    COMMIT TRANSACTION;
    
    -- query window 2
    BEGIN TRANSACTION;
    
        DECLARE @CurrentBalance MONEY;
    
        SELECT @CurrentBalance = [Balance]
        FROM [dbo].[Balance]
        WHERE [BalanceID] = 1;
    
        UPDATE [dbo].[Balance]
        SET [Balance] = @CurrentBalance + 50
        WHERE [BalanceID] = 1;
    
    COMMIT TRANSACTION;
    

    创建表,在单独的查询窗口中执行代码的每一部分。更改隔离级别无济于事。例如,read committedrepeatable read 之间的唯一区别是,最后一个事务在第一个事务完成时阻塞第二个事务,然后覆盖该值。

    【讨论】:

      【解决方案2】:

      我的经验是,使用Read Uncommitted,您不再会“丢失更新”,但是您仍然可以得到“丢失的回滚”。 SQL 培训师可能指的是那个并发问题,所以您可能要寻找的答案是Repeatable Read

      也就是说,如果有人有与此相反的经验,我会非常感兴趣。

      【讨论】:

      • 似乎如果我用Read UncommittedRead Committed 尝试这种情况,它会失败 - 我确实可以“丢失更新”。使用可重复读取,有一个死锁,两个事务之一将被回滚,因此最终不会出现“丢失更新”(但一个事务作为死锁牺牲品被杀死)
      • 但我很困惑,像 Kalen Delaney 这样的知名专家会在她的书中(肯定是经过深入审查)写道,即使使用 Read Uncommitted,“丢失的更新”也不会发生 - 使得我想知道她是否在谈论不同的场景……(如果是的话:想知道那个场景是什么)
      • 也许她的意思是因为UPDATE 语句可以是原子的,并且可以在一个操作中进行读取和写入。
      • 但如果我们只讨论原子UPDATE 语句,那么您真的可以定义“丢失更新”的场景吗?
      • @Seph - 不。这大概就是她说这种情况在任何隔离级别下都不会发生的原因,即使是readuncommitted
      【解决方案3】:

      正如 Francis Rodgers 所指出的,您可以依赖 SQL Server 实现的是,一旦事务更新了某些数据,每个隔离级别都会对数据发出“更新锁”,并拒绝来自另一个事务的更新和写入,无论它是什么它是隔离级别。您可以肯定会涵盖此类丢失的更新。

      但是,如果情况是一个事务读取了一些数据(具有不同于可重复读取的隔离级别),那么另一个事务能够更改此数据并提交它的更改,如果第一个事务随后更新相同的数据但这一次,根据他制作的内部副本,管理系统无法保存。

      您在这种情况下的答案是在第一个事务中使用可重复读取,或者可能在第一个事务中对数据使用一些读锁(我真的不知道是否有信心。我只知道这个锁的存在并且你可以使用它们。也许这会帮助任何对这种方法感兴趣的人Microsoft Designing Transactions and Optimizing Locking)。

      【讨论】:

        【解决方案4】:

        我不知道现在回答是否为时已晚,但我只是在大学学习事务隔离级别,作为我研究的一部分,我发现了这个链接:

        Microsoft Technet

        具体有问题的段落是:

        更新丢失

        可以通过以下两种方式之一来解释丢失的更新。在第一种情况下,当一个事务更新的数据在第一个事务提交或回滚之前被另一个事务覆盖时,就认为发生了丢失更新。 这种类型的SQL Server 2005 中不会发生丢失更新,因为它在任何事务隔离级别下都是不允许的。

        丢失更新的另一种解释是,一个事务(事务#1)将数据读入其本地内存,然后另一个事务(事务#2)更改此数据并提交其更改。在此之后,事务#1 根据在事务#2 执行之前读入内存的内容更新相同的数据。在这种情况下,事务#2 执行的更新可以被视为丢失更新。

        所以本质上两个人都是对的。

        就个人而言(我很容易犯错,所以请纠正我,因为我正在学习这一点)我从中得出以下两点:

        1. 事务环境的全部意义在于防止丢失更新,如上段所述。因此,如果即使是最基本的事务级别都无法做到这一点,那为什么还要使用它呢。

        2. 当人们谈论丢失更新时,他们知道第一段适用,因此一般来说是指第二种丢失更新。

        再次,如果这里有任何错误,请纠正我,因为我也想理解这一点。

        【讨论】:

        • 我实际上想问 OP 问题,因为您的答案选择了大胆。强调的文字实际上提出了问题,而不是解决方案:)
        • @LittleAlien - 我不确定我是否理解。你能澄清一下吗?
        【解决方案5】:

        即使读取和写入在不同的事务中,也可能会丢失更新,例如当用户将数据读入网页然后更新时。在这种情况下,没有隔离级别可以保护您,尤其是在从连接池重用连接时。我们应该使用其他方法,例如 rowversion。 Here is my canned answer.

        【讨论】:

        【解决方案6】:

        书中的例子是文员 A 和文员 B 接收小部件的发货。

        他们都检查了当前的库存,看到有 25 件。店员 A 有 50 个小部件并更新到 75,店员 B 有 20 个小部件,因此更新到 45 会覆盖之前的更新。

        我想她的意思是说这种现象可以通过 Clerk A 在所有隔离级别上避免

        UPDATE Widgets
        SET StockLevel = StockLevel + 50
        WHERE ...
        

        而店员 B 在做

        UPDATE Widgets
        SET StockLevel = StockLevel + 20
        WHERE ...
        

        当然,如果SELECTUPDATE 是作为单独的操作完成的,您需要repeatable read 来避免这种情况,因此该行上的S 锁定会在事务期间保持(这会导致死锁在这种情况下)

        【讨论】:

        • 让初始 SELECT 使用 WITH (UPDLOCK) 提示似乎也可以正常工作,对于任何隔离级别 - 感谢您的输入!
        • @marc_s - 是的。这也避免了死锁风险。
        • @MartinSmith 我知道这是一个旧答案。我尝试使用可重复读取来重现和解决问题,但即使其中一个会话被阻止,在其他会话完成期间,我仍然收到lost update
        猜你喜欢
        • 2022-09-27
        • 1970-01-01
        • 2023-03-04
        • 2011-09-30
        • 2015-07-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-02
        相关资源
        最近更新 更多