Должен ли SELECT... FOR UPDATE всегда содержать ORDER BY?

Скажем, мы выполним...

SELECT * FROM MY_TABLE FOR UPDATE

... и в MY_TABLE больше одной строки.

Теоретически, если две транзакции параллельные выполняют этот оператор, но происходит переход (и, следовательно, блокировка) строк в другом порядке, может возникнуть взаимоблокировка. Например:
  • Транзакция 1: Заблокирует строку A.
  • Транзакция 2: Заблокирует строку B.
  • Транзакция 1: попытки заблокировать строки B и блоки.
  • Транзакция 2: попытка блокировки строки A и взаимоблокировок.

Способом устранения этого является использование ORDER BY, чтобы гарантировать, что строки всегда заблокированы в том же порядке.

Итак, мой вопрос: будет ли этот теоретический тупик когда-либо на практике? Я знаю, что есть способы искусственно побудить его, но может ли это когда-либо происходить при нормальной работе? Должны ли мы всегда использовать ORDER BY, или это действительно безопасно опустить?

В первую очередь меня интересует поведение Oracle и MySQL/InnoDB, но комментарии к другим СУБД также будут полезны.

--- EDIT ---

Вот как воспроизвести тупик в Oracle, когда порядок блокировки не одинаков:

Создайте тестовую таблицу и заполните ее некоторыми тестовыми данными...

CREATE TABLE DEADLOCK_TEST (
 ID INT PRIMARY KEY,
 A INT 
);
INSERT INTO DEADLOCK_TEST SELECT LEVEL, 1 FROM DUAL CONNECT BY LEVEL <= 10000;
COMMIT;

... с одного сеанса клиента (я использовал SQL Developer), запустите следующий блок:

DECLARE
 CURSOR CUR IS 
 SELECT * FROM DEADLOCK_TEST
 WHERE ID BETWEEN 1000 AND 2000 
 ORDER BY ID 
 FOR UPDATE;
BEGIN
 WHILE TRUE LOOP
 FOR LOCKED_ROW IN CUR LOOP
 UPDATE DEADLOCK_TEST 
 SET A = -99999999999999999999 
 WHERE CURRENT OF CUR;
 END LOOP;
 ROLLBACK;
 END LOOP;
END;
/

От другого сеанса клиента (я просто запустил еще один экземпляр SQL Developer), запустите тот же блок, но с DESC в ORDER BY. Через несколько секунд вы получите:

ORA-00060: deadlock detected while waiting for resource

Кстати, вы, скорее всего, достигнете того же результата, полностью удалив ORDER BY (так что оба блока идентичны), и добавив...

ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 1;

... перед одним блоком, но...

ALTER SESSION SET OPTIMIZER_INDEX_COST_ADJ = 10000;

... перед другим (поэтому Oracle выбирает разные планы выполнения и, вероятно, извлекает строки в другом порядке).

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

3 ответа

Ваш пример в вашем вопросе показывает, что порядок блокировки зависит от метода доступа. Этот путь доступа напрямую не определяется предложением ORDER BY запроса, есть много факторов, которые могут влиять на этот путь доступа. Поэтому вы не можете предотвратить тупик, просто добавив ORDER BY, потому что у вас все еще есть два разных пути доступа. Фактически, запустив тестовый пример с порядком и изменив параметры сеанса, я смог вызвать два сеанса для запуска в ORA-60 с тем же запросом.

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

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

Самый простой способ предотвратить тупик - использовать либо FOR UPDATE NOWAIT, либо FOR UPDATE WAIT X (хотя WAIT X все еще может запускать тупик со значениями X, превосходящими механизм обнаружения блокировки, в настоящее время 3 секунды с 11 г, я считаю, - спасибо @APC за исправление).

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

На практике я бы сказал, что большинство приложений с реальными пользователями скорее получат ошибку немедленно, чем транзакция, которая будет бесконечно ждать для завершения другой транзакции. Я бы рассмотрел FOR UPDATE без NOWAIT только для некритических пакетных заданий.


Посмотрите на это по-другому. Запрет причудливых реализаций, опуская предложения ORDER BY, почти наверняка будет давать один и тот же порядок каждый раз, но в том числе и дает возможность программистам "обслуживания" и другим доброжелателям возиться с ними или некоторыми из них, чтобы они некоторая конечная вероятность прекращения разницы в течение срока действия приложения.


Я думаю, вы неправильно поняли, как работает UPDATE. Он получает блокировки, когда курсор активирован, то есть когда выдается SELECT.

Итак, выполнив ваш запрос, Transaction 1 заблокирует всю таблицу (потому что вы не указали предложение WHERE). Транзакция 2 будет либо зависать, либо сбой (в зависимости от того, что вы указали в предложении WAIT) независимо от того, выпустила ли транзакция 1 DML против выбранного набора записей. Если факт, транзакция 1 даже не должна брать какие-либо записи; Транзакция 2 будет бросать ORA-00054 после того, как Transaction 1 откроет курсор FOR UPDATE.

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

Неоценимый г-н Ките предоставляет ключевое понимание в своем блоге:

"обнаружение тупиковой ситуации превосходит период ожидания"

В моем коде я использовал NOWAIT в предложении FOR UPDATE курсора, используемого во втором сеансе:

cursor c10000 is
 select * from order_lines
 where header_id = 1234
 for update;
cursor c1 is
 select * from order_lines
 where header_id = 1234
 and line_id = 9999
 for update nowait;

Следовательно, Сессия 2 немедленно выходит из строя и бросает ORA-00054.

Однако OP ничего не указывает, и в этом случае второй сеанс будет ждать бесконечно для строки, которая будет выпущена. За исключением того, что это не так, потому что через некоторое время обнаружение тупика срабатывает и завершает команду с крайним предрассудком, то есть ORA-00060. Если бы они указали короткий период ожидания - скажите WAIT 1 - они бы увидели ORA-30006: resource busy.

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

open c10000;
loop
 fetch c10000 into r;

или ошеломляющий....

for r in c10000 loop

И действительно не имеет значения, вызвал ли Session 1 интерес к строке при запуске сеанса 2.

TL;DR

Итак, главное, ORDER BY ничего не решает. Первый сеанс для выпуска FOR UPDATE захватывает все записи в результирующем наборе. Любой последующий сеанс, пытающийся обновить любую из этих записей, потерпит неудачу с ORA-00054, ORA-30006 или ORA-00060, в зависимости от того, указаны ли они NOWAIT, WAIT n или ничего.... если только первый сеанс не освобождает блокировки до Время ожидания WAIT или обнаружение блокировки блокировки.

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

declare
 cursor c1 is
 select * from emp
 where deptno = 10
 for update;
 procedure s2 
 is
 cursor c2 is
 select * from emp
 where empno = 7934 -- one of the employees in dept 10
 for update
 -- for update nowait
 -- for update wait 1
 ;
 x_deadlock exception;
 pragma exception_init( x_deadlock, -60);
 x_row_is_locked exception;
 pragma exception_init( x_row_is_locked, -54);
 x_wait_timeout exception;
 pragma exception_init( x_wait_timeout, -30006);
 pragma autonomous_transaction;
 begin
 dbms_output.put_line('session 2 start');
 for r2 in c2 loop
 dbms_output.put_line('session 2 got '||r2.empno);
 update emp
 set sal = sal * 1.1
 where current of c2;
 dbms_output.put_line('session 2 update='||sql%rowcount);
 end loop; 
 rollback;
 exception
 when x_deadlock then
 dbms_output.put_line('session 2: deadlock exception');
 when x_row_is_locked then
 dbms_output.put_line('session 2: nowait exception');
 when x_wait_timeout then
 dbms_output.put_line('session 2: wait timeout exception'); 
 end s2;
begin
 for r1 in c1 loop
 dbms_output.put_line('session 1 got '||r1.empno);
 s2;
 end loop;
end;
/

В этой версии я указал на прямой for update во втором сеансе. Это конфигурация, которую использует OP, и, как видно из выходных данных, поскольку обнаружен тупик:

session 1 got 7782 
session 2 start 
session 2: deadlock exception 
session 1 got 7839 
session 2 start 
session 2: deadlock exception 
session 1 got 7934 
session 2 start 
session 2: deadlock exception 
PL/SQL procedure successfully completed.

То, что это наглядно демонстрирует, - это

  • Первый сеанс заблокировал весь набор результатов из go-get, потому что второй сеанс никогда не блокирует эту строку, даже если первый сеанс еще не получил ее.
  • Исключено исключение Deadlock detected, даже если второй сеанс ничего не смог обновить. 1. Исключено исключение Deadlock detected, даже если первый сеанс не обновляет ни одного из выбранных wows.

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

licensed under cc by-sa 3.0 with attribution.