Стратегии атомарного инкремента и блокировки SQL - это безопасно? - PullRequest
43 голосов
/ 29 сентября 2010

У меня вопрос по SQL и стратегиям блокировки. В качестве примера, предположим, у меня есть счетчик просмотров изображений на моем сайте. Если у меня есть sproc или подобное, чтобы выполнить следующие заявления:

START TRANSACTION;
UPDATE images SET counter=counter+1 WHERE image_id=some_parameter;
COMMIT;

Предположим, что счетчик для конкретного image_id имеет значение '0' в момент времени t0. Если два сеанса, обновляющие один и тот же счетчик изображений, s1 и s2, начинаются одновременно в момент времени t0, есть ли вероятность, что эти два сеанса оба прочитают значение «0», увеличат его до «1» и оба попытаются обновить счетчик до «1 ', поэтому счетчик получит значение' 1 'вместо' 2 '?

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok

Конечный результат: неверное значение '1' для image_id = 15, должно было быть 2.

Мои вопросы:

  1. Возможен ли этот сценарий?
  2. Если это так, имеет ли значение уровень изоляции транзакции?
  3. Существует ли средство разрешения конфликтов, которое обнаружило бы такой конфликт как ошибку?
  4. Можно ли использовать какой-либо специальный синтаксис, чтобы избежать проблемы (что-то вроде сравнения и обмена (CAS) или методов явной блокировки)?

Меня интересует общий ответ, но если его нет, меня интересуют ответы, специфичные для MySql и InnoDB, поскольку я пытаюсь использовать эту технику для реализации последовательностей в InnoDB.

EDIT: Следующий сценарий также может быть возможным, что приводит к тому же поведению. Я предполагаю, что мы находимся на уровне изоляции READ_COMMITED или выше, так что s2 получает значение с начала транзакции, хотя s1 уже записал '1' в счетчик.

s1: begin
s1: begin
s1: read counter for image_id=15, get 0, store in temp1
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: read counter for image_id=15, get 0 (since another tx), store in temp2
s2: write counter for image_id=15 to (temp2+1), which is also 1
s1: commit, ok
s2: commit, ok

Ответы [ 2 ]

29 голосов
/ 29 сентября 2010

UPDATE запрос устанавливает блокировку обновления на страницы или записи, которые он читает.

Когда принимается решение, обновлять ли запись, блокировка либо снимается, либо повышается до эксклюзивной блокировки.

Это означает, что в этом сценарии:

s1: read counter for image_id=15, get 0, store in temp1
s2: read counter for image_id=15, get 0, store in temp2
s1: write counter for image_id=15 to (temp1+1), which is 1 
s2: write counter for image_id=15 to (temp2+1), which is also 1

s2 будет ждать, пока s1 решит, записывать счетчик или нет, и этот сценарий фактически невозможен.

Это будет так:

s1: place an update lock on image_id = 15
s2: try to place an update lock on image_id = 15: QUEUED
s1: read counter for image_id=15, get 0, store in temp1
s1: promote the update lock to the exclusive lock
s1: write counter for image_id=15 to (temp1+1), which is 1 
s1: commit: LOCK RELEASED
s2: place an update lock on image_id = 15
s2: read counter for image_id=15, get 1, store in temp2
s2: write counter for image_id=15 to (temp2+1), which is 2

Обратите внимание, что в InnoDB, DML запросы не снимают блокировки обновления с записей, которые они читают.

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

8 голосов
/ 29 сентября 2010

Если блокировка не выполняется должным образом, безусловно, возможно получить условие гонки этого типа, и режим блокировки по умолчанию (чтение зафиксировано) действительно позволяет это.В этом режиме только чтение устанавливает общую блокировку для записи, поэтому они могут одновременно видеть 0, увеличивать его и записывать 1 в базу данных.

Чтобы избежать этого условия гонки, необходимо установитьэксклюзивный замок на операцию чтения.Режимы параллелизма 'Serializable' и 'Repeatable Read' сделают это, и для операции над одной строкой они в значительной степени эквивалентны.

Чтобы сделать его полностью атомарным, вам необходимо:

  • Установить соответствующий уровень изоляции транзакции , например, Serializable.Обычно это можно сделать из клиентской библиотеки или с помощью явного выражения в SQL.
  • Начать транзакцию
  • Считать данные
  • Обновить их
  • Подтвердить транзакцию.

Вы также можете принудительно установить эксклюзивную блокировку чтения с помощью HOLDLOCK (T-SQL) или эквивалентной подсказки, в зависимости от вашего диалекта SQL.

Один запрос на обновление сделает этоатомарно, но вы не можете разделить операцию (возможно, чтобы прочитать значение и вернуть его клиенту), не гарантируя, что чтения снимают монопольную блокировку. Вам нужно будет получить значение атомарно, чтобы реализовать последовательность , поэтому обновление само по себе, вероятно, не совсем то, что вам нужно. Даже при атомарном обновлении у вас все еще есть условие состязания, чтобы прочитать значение после обновления. Чтение все еще должно произойти в транзакции (сохранение того, что было получено в переменной) и выдать эксклюзивблокировка во время чтения.

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

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

IIRC Oracle поддерживает автономные транзакции, но DB / 2 этого не было до недавнего времени, а SQL Server нет.Я не знаю, поддерживает ли их InnoDB, но Grey и Reuter довольно долго рассказывают о том, насколько сложно их реализовать.На практике, я думаю, вполне вероятно, что это не так.YMMV.

...