如何避免跨数据库 proc 调用的竞争条件?和其他问题

dea*_*ock 6 sql-server stored-procedures t-sql sql-server-2012

我什至不确定这个问题是否有必要,但我很想知道每个人的想法。我在同一台服务器上有两个数据库,dbFoo、dbBar。dbFoo 有下表,请注意这是一个简单的例子,语法可能不正确,因为我很匆忙,对潜在问题的答案更感兴趣,然后是代码......

CREATE TABLE dbo.CodeNumbers(
CodeNumbersID INT IDENTITY (1,1) NOT NULL PRIMARY KEY,
CodeValue VARCHAR(30) NOT NULL
IsUsed BIT NOT NULL DEFAULT(0)
);
Run Code Online (Sandbox Code Playgroud)

dbo.CodeNumbers使用每月提供的 CSV 填充,您选择的导入方法已经写入以将它们放入那里。我们从来没有得到重复的代码。

为了论证,让我们假设表中有 10,000,000 行。导入时都遵循以下格式:

1, 'ajdirjfisofklrlfo039402', 0 all the way till
10000000, 'fkeiir9489', 0
Run Code Online (Sandbox Code Playgroud)

现在在 dbBar 我有 2 个存储过程,第一个应该访问 dbFoo 中第一个未使用的代码,将它返回到一个 out 变量中并将其标记为已使用。所以我有类似的东西:

CREATE PROCEDURE GetNextUseableCode
   @CodeOut VARCHAR(30) OUTPUT,
   @CID INT OUTPUT
AS
   SELECT @CID = CodeNumbersID, @CodeOut = CodeValue
   FROM dbFoo.dbo.CodeNumbers
   WHERE IsUsed = 0

   UPDATE dbFoo.dbo.CodeNumbers
   SET IsUsed = 1
   WHERE CodeNumbersID = @CID 
Run Code Online (Sandbox Code Playgroud)

每天有 20 万个会话在不同时间访问从 dbBar 调用该过程的代码。当dbFoo.Codes没有更多返回时,一切都很好,应用程序只是简单地告诉抱歉明天不再检查。

我有3个主要问题..

  1. 为了避免竞争条件,我需要在代码中有什么特别的东西,如果是这样,最好在不使系统瘫痪的情况下处理这个问题。

  2. 它们是确保在调用过程时获取下一个代码的有效方法,是 ID 列中按时间顺序排列的下一个代码。

  3. 是否还有其他我现在没有考虑的可能会导致大问题的问题,以及处理这种情况的雄辩方法是什么?

我知道这是一个很长的开放式问题,我有一些编码解决方案,但我觉得有更好的方法来获得我想要的结果。

一如既往地提前感谢所有帮助。

Aar*_*and 9

在你的里面没有任何东西SELECT可以支配秩序。它也不受两个会话读取同一行的保护。要查看它是否不安全:

  1. 创建一个带有键列的一次性表。

    CREATE TABLE dbo.GeneratedIDs(ID INT PRIMARY KEY);
    
    Run Code Online (Sandbox Code Playgroud)
  2. 从两个不同的会话循环运行此代码:

    SET NOCOUNT ON;
    
    DECLARE @DECLARE @CID INT, @CodeOUt VARCHAR(30);
    
    SELECT @CID = CodeNumbersID, @CodeOut = CodeValue
      FROM dbFoo.dbo.CodeNumbers
      WHERE IsUsed = 0
    
    UPDATE dbFoo.dbo.CodeNumbers
      SET IsUsed = 1
      WHERE CodeNumbersID = @CID 
    
    INSERT dbo.GeneratedIDs SELECT @CID;
    
    GO 100000
    
    Run Code Online (Sandbox Code Playgroud)
  3. 检查这些输出 - 它们会发生:

    消息 2627,级别 14,状态 1
    违反 PRIMARY KEY 约束“PK_hexcode”。无法在对象“dbo.GeneratedIDs”中插入重复键。重复的键值为 (<some value>)。

    或者,如果将SELECT/包装UPDATE在显式事务中,您可能会看到死锁:

    消息 1205,级别 13,状态 52
    事务(进程 ID 57)在锁定资源上与另一个进程发生死锁,并已被选为死锁牺牲品。重新运行事务。

为了解决这个问题,并确保您获得的 ID 是可用的最低 ID,您可以这样做:

BEGIN TRANSACTION;

SELECT TOP (1) @CID = ...
FROM dbFoo.dbo.CodeNumbers WITH (XLOCK, HOLDLOCK)
WHERE IsUsed = 0
ORDER BY CodeNumbersID;

UPDATE ...

COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)

请注意,我添加了一个显式事务和XLOCK/HOLDLOCK提示,以防止两个同时会话读取同一行。当然,这会对并发性产生影响(不幸的是,这正是您在这里想要和需要的)。

其他方法包括只更新行,然后使用表变量从OUTPUT子句中捕获值:

DECLARE @x TABLE(CodeOut VARCHAR(30), CID INT);

;WITH x AS 
(
  SELECT TOP (1) CodeNumbersID, CodeValue, IsUsed
    FROM dbFoo.dbo.CodeNumbers WITH (XLOCK, HOLDLOCK)
    WHERE IsUsed = 0
    ORDER BY CodeNumbersID
)
UPDATE x SET IsUsed = 1
  OUTPUT inserted.CodeValue, inserted.CodeNumbersID INTO @x;

SELECT @CodeOut = CodeOut, @CID = CID FROM @x;
Run Code Online (Sandbox Code Playgroud)

根据 Paul 的更新,是的,您也可以在没有 table 变量的情况下执行此操作:

;WITH x AS 
(
  SELECT TOP (1) CodeNumbersID, CodeValue, IsUsed
    FROM dbFoo.dbo.CodeNumbers WITH (XLOCK, HOLDLOCK)
    WHERE IsUsed = 0
    ORDER BY CodeNumbersID
)
UPDATE x 
  SET @CodeOut = x.CodeValue, @CID = x.CodeNumbersID, IsUsed = 1;
Run Code Online (Sandbox Code Playgroud)

(虽然我不是很喜欢这种语法;不知道为什么。可能是我总是忘记它存在的相同原因。)

您可以将调用者更改为期望结果集而不是两个输出参数,但这也是额外的工作。在这两种情况下,您仍然需要确保获得最低可用 ID,这可能意味着 CTE 具有SELECT相同的提示。这里有一些讨论。我也在这篇博文中讨论了类似的方法但我没有涉及并发性和试图同时删除同一行的两个会话。显然,在这种情况下,只有他们中的一个可以获胜,但是有了一个UPDATE他们都可以成功(至少在理论上)。

为了使事情更容易,您可以放宽“下一个”ID 是可用的最低 ID 的限制。但是您仍然需要通过提示进行隔离以确保两个同时发生的会话不会碰巧读取相同的值(无论顺序如何都可能发生这种情况)。希望使用合适的索引这些不会破坏并发性。