Параллельный вызов хранимой процедуры для увеличения счетчика и обеспечения атомарных приращений

Я создаю хранимую процедуру, которая может увеличивать значение счетчика и возвращать значение, если этот вызов был ответственен за достижение 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

person Vyrotek    schedule 01.04.2011    source источник
comment
Вам понадобится транзакция, и действительно ли это требование? Для чего это?   -  person John Saunders    schedule 01.04.2011
comment
Да, это реальное требование. Я отслеживаю входящий поток данных, и мне нужно записать, сколько раз данные соответствуют определенным критериям, а затем запустить дополнительную работу, когда будет достигнуто максимальное значение. Текущая система имеет данные и счетчики, хранящиеся в хранилище таблиц Azure, и использует оптимистичный параллелизм для управления тем, кто «победил», выполняя необходимое последнее приращение. Мне нужна более высокая производительность, поэтому я хочу переместить счетчики в SQL Azure.   -  person Vyrotek    schedule 01.04.2011


Ответы (5)


Используйте 1_

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).

person Ryan Elkins    schedule 01.04.2011

Вам нужно обернуть его в транзакцию и добавить выбор в той же транзакции, как показано ниже:

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;

Вы можете поместить эту последнюю часть в выходной параметр, чтобы она возвращалась из процесса.

person squawknull    schedule 01.04.2011
comment
Таким образом, обертывание обоих операторов транзакцией гарантирует, что при вызове запроса Select он вернет строку с изменениями, внесенными только предыдущим оператором обновления? Это означает, что в Proc B не может произойти Update до того, как Proc A выполнит Select? - person Vyrotek; 01.04.2011
comment
@Vyrotek - На самом деле, чтобы получить этот уровень изоляции, вам также необходимо установить уровень изоляции транзакций на Serializable. - person Thomas; 01.04.2011
comment
Не обязательно. Когда вы выполняете обновление, оно автоматически снимает блокировку WRITE для строки. Пока вы не зафиксируете транзакцию, никто другой не сможет в нее писать, и только ДРУГИЕ транзакции, работающие под READ_UNCOMMITTED, могут прочитать обновленное значение до того, как транзакция будет зафиксирована. - person squawknull; 01.04.2011
comment
На самом деле, один комментарий: если вы используете изоляцию моментальных снимков, все может стать немного хуже. Однако большинство людей этого не делают. Он уничтожает tempdb для баз данных с любым значительным объемом, проходящим через них. - person squawknull; 01.04.2011
comment
Спасибо за ваши комментарии, squaknull. Я почти уверен, что все на сервере настроено на уровень изоляции по умолчанию для SQL Server 2008. На данный момент это кажется самым простым решением. - person Vyrotek; 01.04.2011

Один из способов добиться того, что вы ищете, — это придерживаться пессимистического подхода; это означает, что каждая хранимая процедура обновляет запись только в том случае, если она не была изменена другой хранимой процедурой, и повторяет попытку, пока не будет достигнуто максимальное значение. Для этого вам нужно прочитать текущее значение перед обновлением, а затем обновить запись с предложением 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
person Herve Roggero    schedule 01.04.2011

Другая стратегия, которую я изучаю, — это использование sp_getapplock внутри транзакции. Кажется, это позволило бы мне создать уникальную строку для счетчика, который я пытаюсь обновить, и заблокировать другие одновременные выполнения, пока он не будет завершен.

Это кажется особенно полезным, поскольку моя процедура также будет содержать некоторую логику IF EXISTS... ELSE..., которая будет обрабатывать либо создание записи счетчика в первый раз, либо обновление существующей записи.

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

person Vyrotek    schedule 01.04.2011

Предполагая, что MaxValue хорошо известен и одинаков для каждого счетчика, вам не нужны транзакции:

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

Это база данных, а не многопоточная программа. Это запрос к SQL Server на увеличение значения столбца Counter одной строки таблицы. SQL Server сделает это - я не думаю, что это позволит таблице потерять один из запросов.

Так что в худшем случае вы можете получить Counter > MaxValue. Но если вы знаете, что такое MaxValue, то вы знаете, что любое значение выше этого значения на самом деле означает MaxValue. Нет необходимости мгновенно планировать работу в той же транзакции.

Таким образом, в зависимости от того, насколько критической по времени является «дополнительная работа», просто попросите задание или другую программу запросить таблицу в поисках любых значений счетчика, больших или равных MaxValue, и сразу же выполните работу. В худшем случае создайте триггер, который будет срабатывать при каждом ОБНОВЛЕНИИ, который работает только при высоком значении счетчика.

Нет необходимости в транзакциях, если только вам не нужна «дополнительная работа» для выполнения в той же транзакции, которая выполняет обновление счетчика. Поскольку вы не говорите, что сейчас используете для этого транзакции, я подозреваю, что вам не нужна «дополнительная работа» в той же транзакции.

person John Saunders    schedule 01.04.2011
comment
Дополнительная работа, о которой я говорю, — это код C#, который должен запускаться сразу после выполнения процедуры, только если только что выполненное обновление было тем, которое привело к достижению MaxValue. Я не хочу, чтобы CurrentValue когда-либо превышал MaxValue. Я не могу использовать задания, поскольку они недоступны в SQL Azure. - person Vyrotek; 01.04.2011