Subject: зачем нужно NO RECORD_VERSION
From: "Vadim Guchenko" <s0lver@kraslan.ru>
Date: Mon, 28 Jun 2004 20:54:10 +0800

Hello, All! Я проектировал базу данных таким образом, чтобы она не выдавала ошибок блокировки вообще, а в случае возникновения конфликтов ждала, пока блокирующая транзакция завершится, и продолжала свою работу (разумеется о настоящем deadlock здесь речь не идет, когда две транзакции взаимно блокируются, ожидая окончания друг друга, т.к. в нормально спроектированной базе такого быть не должно).

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

Решено было остановится на READ COMMITTED RECORD_VERSION. Прочитав много разных статей и документацию я был убежден, что такая транзакция, запущенная с параметром WAIT, в случае попадания на конфликт при изменении данных будет ждать окончания блокирующей транзакции, и независимо от того, чем та закончится - COMMIT или ROLLBACK, продолжит свою работу по изменению этих данных. Все оказалось не так.

Транзакция READ COMMITED RECORD_VERSION WAIT действительно может изменять данные, которые были изменены другой транзакцией, стартовавшей позже, и подтверждены по COMMIT, но только в том случае, если подтверждение было раньше, чем первая транзакция приступит к изменению этих данных и попадет на конфликт и последующее ожидание. Если случится конфликт, то в случае если блокирующая транзакция откатится, ожидающая транзакция сможет изменить данные и продолжит свою работу, однако если блокирующая транзакция подтвердится, то ожидающая транзакция вместо того, чтобы увидеть подтвержденные данные и изменить их снова, выйдет по ошибке deadlock, что является странным для READ COMMITTED транзакции. В этом случае ее поведение ничем не отличается от транзакции SNAPSHOT и практическое применение параметра WAIT кажется сомнительным.

Пример:

Две транзакции имеют параметры READ WRITE READ COMMITTED RECORD_VERSION WAIT.

  1. Стартовала транзакция1.
  2. Стартовала транзакция 2.
  3. Транзакция 2 изменила данные.
  4. Транзакция 1 пытается изменить те же данные и зависает в ожидании окончания транзакции 2.
  5. Транзакция 2 подтвердила изменения.
  6. Транзакция 1 вместо того, чтобы изменить данные, выдала ошибку deadlock.

Поскольку при обновлении записей всегда происходит их предварительное чтение (это видно в анализе производительности IB Expert'а), то объяснить такое поведение можно следующим (это предположение): транзакция 1 прочитала строку, которую нужно изменить, но при попытке непосредственного изменения натолкнулась на конфликт и стала ждать. А после завершения транзакции 2 оказалось, что версия записи в базе, которую нужно изменить, отличается от той, что была считана, и соответственно происходит выход по ошибке.

После этого разумным было проверить как поведут себя эти же транзакции с уровнем узоляции READ COMMITTED NO RECORD_VERSION. Оказалось, что описанная выше ситуация в этом случае обрабатывается правильно.

Пример:

Две транзакции имеют параметры READ WRITE READ COMMITTED NO RECORD_VERSION WAIT.

  1. Стартовала транзакция 1.
  2. Стартовала транзакция 2.
  3. Транзакция 2 изменила данные.
  4. Транзакция 1 пытается изменить те же данные и зависает в ожидании окончания транзакции 2.
  5. Транзакция 2 подтвердила изменения.
  6. Транзакция 1 изменила те же данные.
  7. Транзакция 1 подтвердила изменения.

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

Пример:

Три транзакции имеют параметры READ WRITE READ COMMITTED NO RECORD_VERSION WAIT.

  1. Стартовала транзакция 1.
  2. Стартовала транзакция 2.
  3. Стартовала транзакция 3.
  4. Транзакция 1 изменила данные.
  5. Транзакция 2 пытается изменить те же данные, но зависает в ожидании окончании транзакции 1, т.к. не может их считать.
  6. Транзакция 3 пытается изменить те же данные, но зависает в ожидании окончании транзакции 1, т.к. не может их считать.
  7. Транзакция 1 подтвердила изменения.
  8. Транзакция 2 считала данные и сейчас будет записывать измененную версию данных.
  9. Транзакция 3 считала данные и сейчас будет записывать измененную версию данных.
  10. Транзакция 2 записывает измененные данные.
  11. Транзакция 3 пытается записать измененные данные, но зависает в ожидании окончания транзакции 2.
  12. Транзакция 2 подтверждает изменения.
  13. Транзакция 3 обнаруживает, что версия записи в базе отличается от той, которую она ранее считала, и должна выдать ошибку.

Однако на практике все проходит без ошибок и все 3 изменения происходят последовательно одно за другим. Это говорит о том, что либо операция update выполняет чтение и запись измененных данных атомарно, либо подобные конфликты в случае с NO RECORD_VERSION WAIT были специально предусмотрены.

В любом случае у NO RECORD_VERSION есть еще одно полезное применение в изменяющих транзакциях, помимо ее бесполезного применения в читающих транзакциях.

With best regards, Vadim Guchenko. E-mail: s0lver@kraslan.ru