Ответ сделан на использование форматирования текста при демонстрации «состояния гонки» в многопоточном программировании (что такое SQL), когда [ab] использует объекты вне транзакции (последовательности SQL, также известные как Firebird Generators).
Итак, «вариант использования».
- Исходное условие: таблица пуста, генератор = 0.
- Вы запускаете две параллельные транзакции, A и B. Для простоты воображения вы можете подумать, что эти транзакции были начаты с одновременных подключений, выполненных двумя людьми, работающими с вашей программой на двух сетевых компьютерах.Хотя на самом деле это не имеет большого значения, если вы откроете им транзакции из одного и того же соединения - сценарий не изменится немного.Просто для простоты воображения.
- Tx.A выдает UPDATE-OR-INSERT, которая вставляет новую строку в таблицу.Делая так, он поднимает тики генератора.Сделка еще не совершена.Условие базы данных: в таблице есть одна невидимая (не зафиксированная) строка с
auto_id=1
, генератор = 1. - Tx.B тоже выдает UPDATE-OR-INSERT, которая вставляет еще одну строку в таблицу.Это также поднимает тики генератора.Транзакция может быть совершена сейчас, а может и позже, не имеет значения.Условие базы данных: таблица имеет две строки (одна или обе невидимы (не зафиксированы)) с
auto_id=1
и auto_id=2
, генератор = 2. - Tx.A встречает некоторую ошибку, выдаетисключение, СКАЧИТ генератор и откатится.Условие базы данных: таблица содержит одну строку с
auto_id=2
генератор = 1. - Если Tx.B не был зафиксирован ранее, он фиксируется сейчас.(это «если» просто для демонстрации того, что не имеет значения, когда будут совершаться другие транзакции, раньше или позже, имеет значение только то, что Tx.A отключает генератор после выполнения любой другой транзакции с его повышением)
Итак, последнее условие базы данных: таблица имеет одну зафиксированную = видимую строку с auto_id=2
и генератор = 1. Любая следующая попытка добавить еще одну строку будет пытаться поднять генератор 1 + 1 = 2, а затем не удастся вставитьновая строка с нарушением PK, затем генератор опустится до 1, чтобы воссоздать неисправное состояние, описанное выше.Ваша база данных застряла и без прямого вмешательства администратора БД не может быть добавлена дополнительная информация.
Сама идея отката генератора заключается в том, чтобы победить все намерения, для которых были созданы генераторы, и все ожидания относительно поведения генераторов, связанные с базой данных и соединениембиблиотеки и другие программисты имеют.Вы просто поставили ловушку на шоссе.Это всего лишь вопрос времени, когда кто-то будет пойман на это.
Даже если вы продолжите охранять этот хак другими хакерами на данный момент - тратите много времени и внимания, чтобы сделать это скрупулезно и повсеместно - все ещев один неудачный день в будущем появится другой программист, или даже вы забудете эти ужасные подробности - и вы начнете использовать генератор стандартным образом - и попадете в ловушку.
Генераторы не созданыбыть возвращенным во время обычной работы.
наличие первичного ключа проверяется в процедуре, прежде чем что-либо предпринимать
Да, это первая реакция, когда многопоточный программистсоответствует его первому состоянию гонки.Давайте просто добавим больше предварительных проверок.
Первые несколько проверок действительно могут уменьшить вероятность столкновения, но никогда не смогут полностью его устранить.И чем больше будет использоваться ваша программа, тем больше транзакций будет открывать все больше и больше одновременно работающих и активных пользователей - это лишь вопрос времени, когда эта несколько сниженная вероятность окажется слишком большой.
Подумайтеоб этом, SQL о транзакциях, но они должны были изобрести и представить явное устройство вне транзакций Генератор / Последовательность.Если бы не было надежного решения без них - его просто использовали бы вместо создания такого не-SQLish инструмента для разграничения границ транзакций.
Когда вы говорите, что ваш SP «проверяет нарушение PK», это точно так же, как если бы вы вообще сбросили генератор и вместо этого просто выпустили «старый добрый»
:new_id = ( select max(auto_id)+1 from MyTable );
По вашему описанию вы действительно делаете что-то подобное, но каким-то косвенным образом. Что-то вроде
while exists( select * from MyTable where auto_id = gen_id(MyGen, +1))
do ;
:new_id = gen_id(MyGen, 0);
Вам может показаться, что, поскольку вы упомянули генераторы , вы каким-то образом преодолели проблему невидимости между транзакциями. Но вы этого не сделали, потому что сама проверка «если PK уже была занята» выполняется в таблице транзакций.
Это ничего не меняет, ваши две транзакции Tx.A и Tx.B не будут видеть записи друг друга, потому что они обе еще не зафиксированы. Теперь требуется только какой-нибудь неудачный Tx.C, который может потерпеть неудачу и установить генератор, чтобы они столкнулись с одним и тем же идентификатором.
Или нет, вам даже не нужен Tx.C и замедление вообще!
Здесь мы наталкиваемся на многопоточную идею об «атомарных операциях».
Давайте посмотрим на это снова.
while exists( select * from MyTable where auto_id = gen_id(MyGen, +1))
do ;
:new_id = gen_id(MyGen, 0);
В однопоточном приложении с этим кодом все в порядке: вы просто продолжаете запускать генератор до свободного места, а затем просто запрашиваете значение, не меняя его. "Что возможно могло пойти не так?" Но в многопоточной среде грачи ждут, чтобы их перешагнули. Пример:
- Исходное условие, таблица имеет 100 строк (
auto_id
идет от 1 до 100), генератор = 100.
- Tx.A начинает добавлять строку, поднимает генератор в цикле while и выходит из цикла. Он еще не переходит на вторую строку, где назначается локальная переменная. Еще нет. Генератор = 101, строки еще не добавлены.
- Tx.B начинает добавлять строку, поднимает генератор в цикле while и выходит из цикла. Генератор = 102, строки еще не добавлены.
- Tx.A переходит на вторую строку и читает
gen_id(MyGen,0)
в переменную для новой строки. Хотя это было 101 из цикла, теперь это 102!
- Tx.B переходит на вторую строку и читает
gen_id(MyGen,0)
и тоже получает 102.
- Tx.A и Tx.B оба пытаются вставить новую строку с auto_id = 102
- УСЛОВИЯ РАСЫ - и Tx.A, и Tx.B пытаются совершить свою работу. Один из них преуспевает, другой терпит неудачу. Который из? Это не предсказуемо. Счастливчик совершает неудачу, неудачник терпит неудачу.
- Неудачная транзакция закрывает генератор.
Конечное условие: в таблице 101 строка, auto_id последовательно увеличивается от 1 до 100, а затем пропускается до 102. Генератор = 101, что его меньше MAX(auto_id)
Теперь вы можете захотеть добавить больше хаков, я имею в виду больше предварительных проверок перед фактической вставкой строк и фиксацией. Это сделает ошибки еще менее вероятными, верно? Неправильно. Чем больше проверок вы делаете - тем медленнее получает код. Чем медленнее получается код - тем больше вероятность того, что, пока один поток запускается, сгенерирует все их проверки, случится другой поток, который вмешивается и изменяет ситуацию, которая была проверена минуту назад.
Основная проблема с многопоточностью заключается в том, что любая проверка является отдельным действием. И между этими действиями ситуация МОЖЕТ измениться. Ваша процедура может проверить все, что захочет, ДО фактической вставки строки. Это не будет оправдывать много. Потому что, когда вы, наконец, доберетесь до оператора вставки строк, все проверки, выполненные вами в PAST, уйдут в прошлое. И ситуация потенциально уже изменилась. И гарантии, что ваши чеки, которые вы давали в ПРОШЛОМ, принадлежат только этому прошлому, а не тому моменту, который у вас под рукой.
И даже если вы больше не ищете гарантированную вещь, добавляя каждую новую проверку, вы даже не можете быть уверены, что, делая это, вы просто уменьшили или увеличили вероятность неудачи. Поскольку многопоточность - сука, она хаотично течет из-под вашего контроля.
Итак, запомните KISS principle
. Пока не доказано обратное - вам, скорее всего, вообще не нужен пакет обновления 2, вам нужен только один оператор UPDATE-OR-INSERT.
PS.В школьные годы была довольно забавная игра, она называлась Pascal Robots .Есть также C-роботы, которых я слышал, и, вероятно, реализация для других языков.Вместе с Pascal Robots появилось несколько уже закодированных роботов, демонстрирующих различные стратегии и подходы.Некоторые из них были действительно продуманы до мелочей.И был один робот, программа которого была ПРИМИТИВНОЙ.У него было только две петли: если вы не видите врага - продолжайте поворачивать свой радар, если вы видите врага - продолжайте бежать к нему и стрелять по нему.Это все.Что может сделать этот идиот против искушенных роботов, имеющих креативные стратегии атаки и защиты, обходные маневры, оптимальное расстояние для движения вперед-назад, трюки на побег и многое другое?Эти изощренные роботы использовали очень обширные проверки и очень продуманные взломы, которые запускаются этими проверками.Итак ... ... чтобы этот примитивный идиот был вторым или, может быть, третьим лучшим роботом в наборе.был только один или два умника, которые могли его перехитрить.Со ВСЕМИ другими роботами этот тупой идиот добил их до того, как они трижды пробежали все свои проверки и взломали.Вот что делает многопоточность с программированием.Удивительно было наблюдать за этими битвами, которые шли против однопоточной интуиции.