并行调用存储过程以增加计数器并确保原子增量

时间:2011-04-01 02:33:54

标签: sql-server-2008 concurrency locking parallel-processing azure-sql-database

我正在创建一个存储过程,它可以递增计数器的值,如果该调用负责到达 MaxValue ,则返回。棘手的部分是这个程序将从不同的线程和不同的机器快速并行地调用。

示例场景:

并行执行的两个线程调用相同的存储过程来递增相同的计数器。假设 CounterId = 5 作为两者的参数传入。在执行计数器记录之前,当前的字段值 CounterValue = 9 MaxValue = 10

我想要发生的是其中一个过程成功地将CurrentValue增加到10并返回一个结果,表明它负责进行导致CounterValue达到MaxValue的更改。另一个程序不应该增加该值(因为它将超过10)并且应该返回一个结果,表明已经满足了计数器的MaxReach。

我考虑过在之前或之后执行查询,但似乎可以留下一个“漏洞”,可以通过单独的线程进行更改并导致返回误报/负面。

这只是该程序想法的开始。我觉得它需要锁定,交易还是什么?

UPDATE SomeCounters
SET CounterValue = (CounterValue + @AddValue),
    MaxReached = CASE WHEN MaxValue = (CurrentValue + 1) THEN 1 ELSE 0
WHERE CounterId = @CounterId
  AND MaxReached = 0

5 个答案:

答案 0 :(得分:3)

使用OUTPUT

DECLARE @temp TABLE (MaxReached BIT NOT NULL);

UPDATE SomeCounters
  SET CounterValue = (CounterValue + @AddValue),
      MaxReached = CASE WHEN MaxValue = (CurrentValue + 1) THEN 1 ELSE 0
  WHERE CounterId = @CounterId
    AND MaxReached = 0
  OUTPUT INSERTED.MaxReached INTO @temp

更新是原子的,然后您可以从@temp表中选择值,并随意执行任何操作。这样,您将能够捕获导致MaxReached设置为true的确切更新(1)。

答案 1 :(得分:1)

您需要将其包装在事务中并在同一事务中添加select,如下所示:

BEGIN TRANSACTION; 

UPDATE SomeCounters
SET CounterValue = (CounterValue + @AddValue)
WHERE CounterId = @CounterId;

SELECT CASE WHEN MaxValue = CurrentValue THEN 1 ELSE 0 MaxReached
FROM SomeCounters
WHERE CounterId = @CounterId;

COMMIT TRANSACTION;

您可以将最后一部分放入输出参数,以便从proc。

返回

答案 2 :(得分:0)

实现目标的一种方法是采取悲观态度;意味着每个存储过程仅在未被另一个存储过程修改时更新记录,并再次尝试直到达到最大值。为此,您需要在更新之前读取当前值,然后使用期望值相同的WHERE子句更新记录。如果您需要确保呼叫最终成功,您还需要一个循环。使用此方法,一次只有1个存储过程将更新表,并重试工作直到达到最大值。

这样的事情:

DECLARE @savedValue int
DECLARE @maxedReached int
-- read current values for concurrency
SELECT @savedValue = CounterValue, @maxedReached = MaxReached 
  FROM SomeCounters WHERE CounterId = @counterId)

WHILE(@maxedReached = 0)
BEGIN

  UPDATE SomeCounters
  SET CounterValue = (CounterValue + @AddValue),    
    MaxReached = CASE WHEN MaxValue = (CurrentValue + 1) THEN 1 ELSE 0 END
  WHERE 
    CounterId = @CounterId  
    AND MaxReached = 0
    -- the next clause ensures that only one stored procedure will succeed
    AND CounterValue = @savedValue  

  if (@@rowcount = 0)
  BEGIN
    -- failed... another procedure made the change?
    -- If @maxReached becomes 1, the loop will exit and you will
    -- know the maximum was reached; if not the loop will try updating
    -- the value again
    -- read the values for concurrency again.
    SELECT @savedValue = CounterValue, @maxedReached = MaxReached 
        FROM SomeCounters WHERE CounterId = @counterId)

  END
END

答案 3 :(得分:0)

我正在研究的另一个策略是在事务中使用sp_getapplock。这似乎允许我为计数器创建一个唯一的字符串,我试图更新并阻止其他并发执行,直到它完成。

这似乎特别有用,因为我的程序还将包含一些IF EXISTS ... ELSE ...逻辑,它将处理第一次创建计数器记录或更新现有计数器。

http://msdn.microsoft.com/en-us/library/ms189823.aspx - sp_getapplock

答案 4 :(得分:-1)

假设MaxValue众所周知,并且每个计数器都相同,那么您不需要交易:

UPDATE CounterTable
SET Counter=Counter+1
WHERE CounterId = @CounterId

这是一个数据库,而不是一个多线程程序。这是对SQL Server的请求,以递增表的一行的Counter列的值。 SQL Server会这样做 - 我认为它不会允许表丢失其中一个请求。

所以,最糟糕的是,你最终可能会遇到Counter > MaxValue。但是如果你知道MaxValue是什么,那么你知道它上面的任何值都真正意味着MaxValue。无需在同一交易中立即安排工作

因此,根据“额外工作”的时间关键程度,只需要一个工作或其他程序查询该表,查找大于或等于MaxValue的任何计数器值,并在那里完成工作。在最坏的情况下,创建一个触发器以在每个UPDATE上运行,只有在计数器值很高时才会执行任何操作。

不需要事务,除非您需要在执行计数器更新的同一事务中执行“额外工作”。由于您现在没有说您正在使用交易,我怀疑您不需要在同一交易中进行“额外工作”。