代码之家  ›  专栏  ›  技术社区  ›  JohnLBevan

为什么原子语句上需要锁提示?

  •  3
  • JohnLBevan  · 技术社区  · 7 年前

    问题

    对下面的语句应用锁有什么好处?

    同样,如果不包含这些提示,我们会看到什么问题?i、 e.他们是否会阻止比赛状态,提高表现,或者其他什么?询问,因为他们可能被包括在内,以防止一些我没有考虑的问题,而不是我假设的种族状况。

    注意:这是从这里提出的一个问题中溢出的: SQL Threadsafe UPDATE TOP 1 for FIFO Queue

    有关声明

    WITH nextRecordToProcess AS
    (
        SELECT TOP(1) Id, StatusId
        FROM    DemoQueue
        WHERE   StatusId = 1 --Ready for processing
        ORDER BY DateSubmitted, Id 
    )
    UPDATE nextRecordToProcess
    SET StatusId = 2 --Processing
    OUTPUT Inserted.Id 
    

    要求

    • SQL用于从队列中检索未处理的记录。
    • 要获取的记录应该是队列中状态为Ready(StatusId=1)的第一条记录。
    • 可能有多个工作进程/会话正在处理来自此队列的消息。
    • 我们希望确保队列中的每个记录只拾取一次(即由单个工作进程拾取),并且每个工作进程按照消息在队列中出现的顺序处理消息。
    • 一个工作人员的工作速度可以比另一个工作人员快(即,如果工作人员A拾取记录1,则工作人员B拾取记录2,如果工作人员B在工作人员A完成记录1之前完成记录2的处理,则可以)。我们只关心在收集记录的情况下。
    • 没有正在进行的交易;i、 e.我们只想从队列中提取记录;我们不需要一直锁定它,直到我们回来从 Processing Processed .

    上下文的其他SQL:

    CREATE TABLE Statuses
    (
        Id SMALLINT NOT NULL PRIMARY KEY CLUSTERED
        , Name NVARCHAR(32) NOT NULL UNIQUE
    )
    GO
    INSERT Statuses (Id, Name)
    VALUES (0,'Draft')
    , (1,'Ready')
    , (2,'Processing')
    , (3,'Processed')
    , (4,'Error')
    GO
    CREATE TABLE DemoQueue
    (
        Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED
        , StatusId SMALLINT NOT NULL FOREIGN KEY REFERENCES Statuses(Id)
        , DateSubmitted DATETIME --will be null for all records with status 'Draft'
    )
    GO
    

    建议的声明

    在讨论队列的各种博客中,以及引发此讨论的问题中,建议将上述语句更改为包含锁提示,如下所示:

    WITH nextRecordToProcess AS
    (
        SELECT TOP(1) Id, StatusId
        FROM    DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
        WHERE   StatusId = 1 --Ready for processing
        ORDER BY DateSubmitted, Id 
    )
    UPDATE nextRecordToProcess
    SET StatusId = 2 --Processing
    OUTPUT Inserted.Id 
    

    我的理解

    我知道需要锁定这些提示的好处是:

    • UPDLOCK:因为我们正在选择要更新其状态的记录,所以我们需要确保在读取该记录之后但在更新之前读取该记录的任何其他会话都无法读取要更新的记录(或者更确切地说,这样的语句必须等到我们执行了更新并释放了锁之后,其他会话才能看到我们的记录及其新值)。
    • ROWLOCK:当我们锁定记录时,我们希望确保我们的锁定只影响我们锁定的行;i、 e.由于我们不需要锁定许多资源/我们不想影响其他进程/我们希望其他会话能够读取队列中的下一个可用项目,即使该项目与我们锁定的记录位于同一页面中。
    • READPAST:如果另一个会话已经在从队列中读取项目,而不是等待该会话释放其锁定,那么我们的会话应该选择队列中的下一个可用(未锁定)记录。

    i、 e.如果我们运行以下代码,我认为这是有意义的:

    DECLARE @nextRecordToProcess BIGINT
    
    BEGIN TRANSACTION
    
    SELECT TOP (1) @nextRecordToProcess = Id
    FROM    DemoQueue WITH (UPDLOCK, ROWLOCK, READPAST)
    WHERE   StatusId = 1 --Ready for processing
    ORDER BY DateSubmitted, Id 
    
    --and then in a separate statement
    
    UPDATE DemoQueue
    SET StatusId = 2 --Processing
    WHERE Id = @nextRecordToProcess
    
    COMMIT TRANSACTION
    
    --@nextRecordToProcess is then returned either as an out parameter or by including a `select @nextRecordToProcess Id`
    

    然而,当select和update出现在同一条语句中时,我会假设没有其他会话可以在我们会话的read&使现代化因此,不需要显式锁定提示。

    我是否从根本上误解了锁的工作原理;或者这些提示的建议是否与其他类似但不同的用例相关?

    2 回复  |  直到 7 年前
        1
  •  2
  •   Remus Rusanu    7 年前

    John说得对,因为这些都是优化,但在SQL世界中,这些优化可能意味着“快”与“无法承受的数据大小慢”之间的区别,和/或“工作”与“无法使用的死锁混乱”之间的区别。

    readpast提示很清楚。对于其他两个,我觉得我需要添加更多的上下文:

    • ROWLOCK提示用于防止页面锁定粒度扫描。锁粒度(行与页)是在查询开始时预先确定的,并基于对查询将扫描的页数的估计(第三个粒度,table,仅在特殊情况下使用,此处不适用)。通常,出列操作不必扫描这么多页面,以便引擎考虑页面粒度。但我已经看到了引擎决定使用页面锁粒度的“野外”情况,这会导致出列时出现阻塞和死锁
    • 需要UPDLOCK来防止升级锁死锁场景。UPDATE语句在逻辑上分为搜索需要更新的行,然后更新这些行。搜索需要锁定其计算的行。如果该行符合条件(满足WHERE条件),则该行将被更新,并且更新始终是独占锁。因此,问题是如何在搜索过程中锁定行?如果使用共享锁,则两个更新将查看同一行(因为共享锁允许,所以它们可以),它们都决定该行符合条件,并且都尝试将锁升级为独占->僵局如果在搜索过程中使用排他锁,则不会发生死锁,但更新将在使用任何其他读取计算的所有行上发生冲突,即使该行不符合条件(更不用说排他锁不能在不中断的情况下提前释放 two-phase-locking ). 这就是为什么有一个U模式锁,一个与共享模式锁兼容(这样候选行的更新计算不会阻止读取),但与另一个U模式锁不兼容(这样两个更新不会死锁)。典型的基于CTE的出列需要此提示的原因有两个:

      1. 由于是CTE,查询处理并不总是理解CTE中的SELECT是更新的目标,应该使用U模式锁和
      2. 出列操作总是在相同的行之后进行更新(这些行正在“出列”),因此死锁很常见。
        2
  •  1
  •   JohnLBevan    7 年前

    tl;博士

    它们用于高并发专用队列表场景中的性能优化。

    冗长的

    我想我已经找到了答案 related SO answer 通过 this quoted blog's 著者

    这条建议似乎是针对一个非常具体的场景;其中用作队列的表是 献身的 作为队列;i、 e.本表不用于任何其他目的。在这种情况下,锁提示是有意义的。他们与防止比赛条件无关;它们通过避免(非常短期的)阻塞来提高高并发场景中的性能。

    • 这个 ReadPast 锁提高了高并发场景下的性能;无需等待当前读取的记录被释放;唯一锁定它的是另一个“队列工作者”进程,因此我们可以安全地跳过知道该工作者正在处理此记录。
    • 这个 RowLock 确保我们一次不锁定多行,因此下一个请求消息的工作者将获得下一条记录,而不是跳过多条记录,因为它们位于锁定记录的页面中。
    • 这个 UpdLock 用于获取锁;即 行锁定 说要锁什么,但没有说必须有锁,以及 读取过去 确定遇到其他锁定记录时的行为,因此不会再次导致锁定当前记录。我怀疑这不是明确需要的,因为SQL无论如何都会在后台获取它(实际上,仅在链接的SO答案中 读取过去 规定);但是为了完整性/为了显式显示SQL在后台隐式导致的锁,被包含在block post中。

    然而 那篇文章是为 专用队列表 . 表格用于其他用途(例如 the original question 这是一个保存发票数据的表格,碰巧有一列用于跟踪打印内容),建议可能不可取。i、 e.通过使用 读取过去 锁定您正在跳过所有锁定的记录;而且也不能保证这些记录被处理您队列的另一个工作人员锁定;它们可能出于完全无关的目的而被锁定。这将打破先进先出的要求。

    鉴于此,我认为 my answer 关于相关问题。i、 e.创建一个专用表来处理队列场景,或者在上下文或您的场景中考虑其他选项及其优缺点。