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

发布于 2024-10-29 02:20:07 字数 744 浏览 4 评论 0原文

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

示例场景:

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

I'm creating a stored procedure which can increment the value of a counter and return if that invocation was responsible for reaching the MaxValue. The tricky part is this procedure will be call quickly and in parallel from different threads and different machines.

Example scenario:

Two threads executing in parallel call the same stored procedure to increment the same counter. Lets assume CounterId = 5 is passed in as a parameter for both. Before either executes the Counter record currently has field values of CounterValue = 9 and a MaxValue = 10.

What I want to happen is for one of the procedures to successfully increment the CurrentValue to 10 and return a result indicating it was responsible for making the change which caused CounterValue to reach the MaxValue. The other procedure should not increment the value (since it would go past 10) and should return a result indicating that the MaxReach was already met for the counter.

I thought about performing a query before or after but it seems that could leave a 'hole' where a change could be made by separate thread and cause a false positive/negative to be returned.

This is just a start of an idea for the procedure. I feel like it needs locking, a transaction or something?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

晨敛清荷 2024-11-05 02:20:07

使用 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) 的确切更新。

Use 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

The update is atomic and you can then select the value out of the @temp table and do whatever you want with it. This way you'll be able to capture the exact update that caused MaxReached to be set to true (1).

隔岸观火 2024-11-05 02:20:07

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

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;

您可以将最后一部分放入输出参数中,以便从过程中返回它。

You need to wrap it in a transaction and add a select within the same transaction, as follows:

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;

You can put that last part into an output parameter so that it's returned from the proc.

残月升风 2024-11-05 02:20:07

实现你所寻求的目标的一种方法是采取悲观的态度。这意味着每个存储过程仅更新未被另一个存储过程修改的记录,然后重试,直到达到最大值。为此,您需要在更新之前读取当前值,然后使用期望值相同的 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

One way to achieve what you are looking for is to take a pessimistic approach; meaning that each stored procedure only updates a record if it wasn't modified by another stored procedure, and try again until the max is reached. To do this you need to read the current value before the update, then update the record with a WHERE clause that expects the value to be the same. You also need a loop if you need to make sure the call eventually succeeds. Using this approach only 1 stored procedure will update the table at a time, and retry the work until the max is reached.

Something like this:

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
深者入戏 2024-11-05 02:20:07

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

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

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

Another strategy I'm investigating is the use of sp_getapplock within a transaction. It seems this would allow me create a unique string for the counter I'm trying to update and block other concurrent executions until it is finished.

This seems particularly useful since my procedure will also contain some IF EXISTS ... ELSE ... logic which will handle either creating the counter record for the first time or updating and existing one.

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

却一份温柔 2024-11-05 02:20:07

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

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

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

因此,在最坏的情况下,您可能会得到 Counter >最大值。但如果您知道 MaxValue 是什么,那么您就会知道任何高于它的值实际上都意味着 MaxValue。无需立即在同一事务中安排工作。

因此,根据“额外工作”的时间紧迫性,只需让一个作业或其他程序查询表,查找大于或等于 MaxValue 的任何计数器值,然后就在那里完成工作。最坏的情况是,创建一个在每次更新时触发的触发器,该触发器仅在计数器值较高时才起作用。

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

Assuming that MaxValue is well-known, and is the same for each counter, then you don't need transactions:

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

This is a database, not a multi-threaded program. This is a request to SQL Server to increment the value of the Counter column of one row of the table. SQL Server will do that - I don't think that it will permit the table to lose one of the requests.

So, at worst, you might wind up with Counter > MaxValue. But if you know what MaxValue is, then you know that any value above it really means MaxValue. There's no need to instantly schedule the work in the same transaction.

So, depending on how time-critical the "extra work" is, simply have a job or other program query the table looking for any counter values greater or equal to MaxValue, and do the work right there. At worst, create a trigger to go off on every UPDATE, which only does any work when the counter value is high.

No need for transactions, unless you need the "extra work" to execute in the same transaction that does the counter update. Since you don't say that you're using transactions for that now, I suspect that you don't need the "extra work" to occur in the same transaction.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文