【问题标题】:SQLServer lock table during stored procedure存储过程期间的 SQLServer 锁定表
【发布时间】:2010-11-10 10:40:04
【问题描述】:

我有一个表,我需要在 99% 的情况下自动分配 ID(另外 1% 的情况似乎排除了使用标识列)。所以我有一个存储过程来获取下一个 ID,如下所示:

select @nextid = lastid+1 from last_auto_id
check next available id in the table...
update last_auto_id set lastid = @nextid

检查必须检查用户是否手动使用了 ID 并找到下一个未使用的 ID。

当我连续调用它时它工作正常,返回 1、2、3 ...我需要做的是提供一些锁定,多个进程同时调用它。理想情况下,我只需要它专门锁定该代码周围的 last_auto_id 表,以便第二次调用必须等待第一次调用更新表,然后才能运行它的选择。

在 Postgres 中,我可以执行“LOCK TABLE last_auto_id;”之类的操作显式锁定表。任何想法如何在 SQL Server 中完成它?

提前致谢!

【问题讨论】:

    标签: sql sql-server locking


    【解决方案1】:

    在更新之后,您的 lastid 会加一,并在单个事务中将此值分配给您的本地变量。

    编辑

    感谢 Dave 和 Mitch 指出原始解决方案的隔离级别问题。

    UPDATE  last_auto_id WITH (READCOMMITTEDLOCK)
    SET     @nextid = lastid = lastid + 1
    

    【讨论】:

    • 如果隔离级别设置为未提交读,我想这可能会导致脏读解析不正确的结果。我已更改答案以包含锁定提示。
    • 隔离级别不够高。您需要可重复阅读或可序列化。它也无法编译。
    • 为什么隔离级别不够?我想每个人都使用该程序来更改表 last_auto_id。
    • @asc99c,您已经有了一个可行的解决方案,但您能否验证我的(最终)编辑是否适合您?我很想知道。
    • Lieven,问题是我还没有真正发布整个程序正在做什么。我编辑了我的回答帖子以显示完整的 SQL。如果我只向 ID 添加 1,您的解决方案应该没问题。在正常情况下,我只添加 1。但在结果 ID 已经在使用的边缘情况下,它会做一些额外的工作来获得下一个未使用的 ID。
    【解决方案2】:

    你们之间已经回答了我的问题。我正在回复自己的回复,以整理我在一篇帖子中找到的工作解决方案。关键似乎是事务方法,在 last_auto_id 表上有锁定提示。将事务隔离设置为可序列化似乎会产生死锁问题。

    这就是我所得到的(经过编辑以显示完整的代码,希望我能得到一些进一步的答案......):

    DECLARE @Pointer AS INT
    
    BEGIN TRANSACTION
    
    -- Check what the next ID to use should be
    SELECT @NextId = LastId + 1 FROM Last_Auto_Id WITH (TABLOCKX) WHERE Name = 'CustomerNo'
    
    -- Now check if this next ID already exists in the database
    IF EXISTS (SELECT CustomerNo FROM Customer
               WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo = @NextId)
    BEGIN
      -- The next ID already exists - we need to find the next lowest free ID
      CREATE TABLE #idtbl ( IdNo int )
    
      -- Into temp table, grab all numeric IDs higher than the current next ID
      INSERT INTO #idtbl
      SELECT CAST(CustomerNo AS INT) FROM Customer
      WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo >= @NextId
      ORDER BY CAST(CustomerNo AS INT)
    
      -- Join the table with itself, based on the right hand side of the join
      -- being equal to the ID on the left hand side + 1.  We're looking for
      -- the lowest record where the right hand side is NULL (i.e. the ID is
      -- unused)
      SELECT @Pointer = MIN( t1.IdNo ) + 1 FROM #idtbl t1
      LEFT OUTER JOIN #idtbl t2 ON t1.IdNo + 1 = t2.IdNo
      WHERE t2.IdNo IS NULL
    END
    
    UPDATE Last_Auto_Id SET LastId = @NextId WHERE Name = 'CustomerNo'
    
    COMMIT TRANSACTION
    
    SELECT @NextId
    

    这会在事务开始时取出一个排他表锁,然后成功地将任何进一步的请求排队,直到该请求更新表并提交它的事务。

    我编写了一些 C 代码来处理来自六个会话的并发请求,它运行良好。

    但是,我确实有一个担心,那就是锁定“提示”这个术语——有谁知道 SQLServer 是否将其视为明确的指令或只是一个提示(即,它可能不会总是遵守它??)

    【讨论】:

      【解决方案3】:

      这个解决方案如何? 无需 TABLE LOCK 即可完美运行!!!

      DECLARE  @NextId INT
      
      UPDATE   Last_Auto_Id 
      SET      @NextId = LastId = LastId + 1
      WHERE    Name = 'CustomerNo'
      
      SELECT   @NextId 
      

      Update statement always uses a lock to protect its update.

      【讨论】:

        【解决方案4】:

        您可能需要考虑死锁。这通常发生在多个用户同时使用存储过程时。为了避免死锁并确保用户的每个查询都会成功,您需要在更新失败期间进行一些处理,为此您需要尝试捕获。这仅适用于 Sql Server 2005,2008。

        DECLARE @Tries tinyint
        
        SET @Tries = 1
        
        WHILE @Tries <= 3
        
        BEGIN
        
          BEGIN TRANSACTION
        
          BEGIN TRY
        
        -- this line updates the last_auto_id
        
        update last_auto_id set lastid = lastid+1
        
           COMMIT
        
           BREAK
          END TRY
        
          BEGIN CATCH
        
           SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() as ErrorMessage
        
           ROLLBACK
        
           SET @Tries = @Tries + 1
        
           CONTINUE
        
         END CATCH
        
        END
        

        【讨论】:

        • 这只是执行重试。它没有说任何关于锁定的内容。
        • 实际上它可能是一个有用的解决方案,因为当我最初尝试使用事务时(当我使用 SERIALIZABLE 隔离级别时)我遇到了死锁。不完全确定 SQL Server 认为死锁在哪里,但这可能已经对其进行了排序。
        • @Dave .. 像多个用户尝试同时访问同一个过程这样的情况可能会导致死锁,尤其是当更新或插入等重要过程发生时。在 asc99c 的情况下,他需要接受请求并从表中获取最新的 id。我试图建议最好的方式来接受多个用户同时访问同一个表的相同请求。您可能想尝试阅读此链接以更好地理解我的真正意思。 msdn.microsoft.com/en-us/library/aa175791%28SQL.80%29.aspx
        • @asc99c 如果对您有帮助,请投票,如果它解决了您的问题,请接受。谢谢!
        【解决方案5】:

        我更喜欢在第二个表中使用identity 字段来执行此操作。如果您创建lastididentity,那么您所要做的就是在该表中插入一行并选择@scope_identity 以获取您的新值,并且您仍然拥有identity 的并发安全性,即使您的id 字段在您的主表不是identity

        【讨论】:

        • 唯一的问题是身份达到手动使用的大范围值时的性能。该代码用于生成客户 ID。当用户从另一个系统导入客户块时,如果该系统中尚未使用这些 ID,他们可以尝试重新使用现有的客户 ID。因此,可能是从另一个系统加载了从 10,000 到 45,000 的 ID 块。当自动编号达到 10,000 时,它需要跳转到 45,001。令人讨厌的小设计功能,但在接管现有系统时往往是这样。
        • +1。这对您来说是最好的解决方案——它的扩展性比锁定整个表要好得多,而且从长远来看它会更可靠。如果有人出现并编辑您的过程而不在并发环境中对其进行测试,则使用表格提示可能会很危险。
        • @asc99c:您仍然可以使用 IDENTITY。当有人执行数据加载时,您可以使用 SET IDENTITY_INSERT yourtablename ON。如果您想在批量加载情况下重新设置身份值,请使用 DBCC CHECKIDENT(参见在线书籍)重新设置身份种子值以从更高的数字开始。
        猜你喜欢
        • 2017-12-25
        • 2020-04-26
        • 2010-11-02
        • 1970-01-01
        • 1970-01-01
        • 2013-11-06
        • 2011-02-24
        • 1970-01-01
        • 2023-03-12
        相关资源
        最近更新 更多