Как избежать состояния гонки базы данных при ручном увеличении PK новой строки - PullRequest
6 голосов
/ 30 марта 2009

У меня есть устаревшая таблица данных в SQL Server 2005, в которой есть PK без идентификатора / автоинкремента и нет возможности его реализовать.

В результате я вынужден создавать новые записи в ASP.NET вручную с помощью метода ole «SELECT MAX (id) + 1 FROM table» - перед вставкой.

Очевидно, что это создает состояние гонки для ID в случае одновременных вставок.

Какой лучший способ изящно разрешить случай столкновения гонки? Я ищу идеи кода VB.NET или C # по принципу обнаружения столкновения и повторной попытки неудачной вставки, получая еще один максимум (id) + 1. Можно ли это сделать?

Мысли? Комментарии? Мудрость?

Спасибо!

ПРИМЕЧАНИЕ. Что делать, если я не могу каким-либо образом изменить базу данных?

Ответы [ 7 ]

6 голосов
/ 30 марта 2009

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

4 голосов
/ 30 марта 2009

Не удалось изменить схему базы данных.

Если вы вставите существующий PK в таблицу, вы получите SqlException с сообщением, указывающим на нарушение ограничения PK. Поймайте это исключение и повторите вставку несколько раз , пока не добьетесь успеха. Если вы обнаружите, что частота столкновений слишком высока, вы можете попробовать max(id) + <small-random-int> вместо max(id) + 1. Обратите внимание, что при таком подходе ваши идентификаторы будут иметь пробелы, и пространство идентификаторов будет исчерпано раньше.

Другой возможный подход - эмулировать идентификатор автоинкрементации вне базы данных. Например, создайте статическое целое число, Interlocked.Increment, каждый раз, когда вам нужен следующий идентификатор и используйте возвращаемое значение. Самое сложное - инициализировать этот статический счетчик с хорошим значением. Я бы сделал это с Interlocked.CompareExchange:

class Autoincrement {
  static int id = -1;
  public static int NextId() {
    if (id == -1) {
      // not initialized - initialize
      int lastId = <select max(id) from db>
      Interlocked.CompareExchange(id, -1, lastId);
    }
    // get next id atomically
    return Interlocked.Increment(id);
  }
}

Очевидно, что последний работает, только если все вставленные идентификаторы получены с помощью Autoincrement.NextId одного процесса.

3 голосов
/ 30 марта 2009

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

Лично я бы использовал SQL Express , потому что это бесплатно и просто.

Если у вас один веб-сервер: Создайте базу данных SQL Express на веб-сервере с одной таблицей [ids] и одним автоинкрементным полем [new_id]. Вставьте запись в эту таблицу [ids], получите [new_id] и передайте ее на слой вашей базы данных в качестве PK рассматриваемой таблицы.

Если у вас несколько веб-серверов: Это неудобно в настройке, но вы можете использовать тот же трюк, установив соответствующее начальное значение / приращение (т.е. приращение = 3 и начальное значение = 1/2/3 для трех веб-серверов).

3 голосов
/ 30 марта 2009

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

Вы можете сделать это?

INSERT (PKcol, col2, col3, ...)
SELECT (SELECT MAX(id) + 1 FROM table WITH (HOLDLOCK, UPDLOCK)), @val2, @val3, ...

Без тестирования это, вероятно, тоже будет работать:

INSERT (PKcol, col2, col3, ...)
VALUES ((SELECT MAX(id) + 1 FROM table WITH (HOLDLOCK, UPDLOCK)), @val2, @val3, ...)

Если вы не можете, другой способ сделать это в триггере.

  1. Триггер является частью транзакции INSERT
  2. Используйте HOLDLOCK, UPDLOCK для MAX. Это удерживает блокировку строки до фиксации
  3. Обновляемая строка блокируется на время

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

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

Или измените схему в соответствии с предложением ...

2 голосов
/ 30 марта 2009

Как насчет запуска всего пакета (выберите для id и вставки) в сериализуемой транзакции?

Это должно помочь вам внести изменения в базу данных.

1 голос
/ 30 марта 2009

Является ли основной проблемой одновременный доступ? Я имею в виду, будут ли множественные экземпляры вашего приложения (или, не дай Бог, другие приложения вне вашего контроля) одновременно выполнять вставки?

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

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

Единственная мысль, которая у меня возникла, заключается в том, что если вы хотя бы контролируете, кто имеет доступ к базе данных (например, только «авторизованные» приложения, написанные или одобренные вами), вы можете реализовать мьютекс боковой полосы: виды, где «говорящая палка» является общей для всех приложений, и для выполнения вставки требуется владение мьютексом. Это был бы его собственный волосатый шарик воска, так как вам нужно было бы определить политику для мертвых клиентов, где они находятся, проблемы с конфигурацией и т. Д. И, конечно же, «мошеннический» клиент мог бы выполнять вставки без говорящей палки и шланг всей установки.

1 голос
/ 30 марта 2009

Лучшее решение - изменить базу данных. Возможно, вам не удастся изменить столбец на столбец идентификаторов, но вы должны быть в состоянии убедиться, что для столбца существует уникальное ограничение, и добавить новый столбец идентификаторов, заполненный вашими существующими PK. Затем либо используйте новый столбец, либо используйте триггер, чтобы старый столбец отражал новый, или оба.

...