используя транзакцию, чтобы избежать гонки - PullRequest
1 голос
/ 28 апреля 2009

Я пишу демон для мониторинга создания новых объектов, который добавляет строки в таблицу базы данных, когда обнаруживает новые вещи. Мы будем называть объекты виджетами. Поток кода примерно такой:

1: every so often:
2:   find newest N widgets (from external source)
3:   foreach widget
4:     if( widget not yet in database )
5:       add rows for widget

Последние две строки представляют состояние гонки, так как, если два экземпляра этого демона работают одновременно, они могут оба создать строку для виджета X, если синхронизируется время.

Наиболее очевидным решением было бы использование ограничения unique для столбца идентификатора виджета, но это невозможно из-за разметки базы данных (на самом деле разрешено иметь более одной строки для виджета, но демон никогда не должен делать это автоматически).

Моя следующая мысль будет заключаться в использовании транзакции, поскольку это то, для чего они предназначены. Я полагаю, что в мире ADO.NET я бы хотел уровень изоляции Serializable, но я не уверен. Может ли кто-нибудь указать мне правильное направление?

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

Thread A: Executes line 4, acquiring a read lock on the table
Thread B: Executes line 4, acquiring a read lock on the table
Thread A: Tries to execute line 5, which requires upgrading to a write lock
   (this requires waiting until Thread B unlocks the table)
Thread B: Tries to execute line 5, again requiring a lock upgrade
   (this requires waiting until Thread A unlocks)

Это оставляет нас в классическом тупиковом состоянии. Возможны и другие пути кода, но если потоки A и B не чередуются, проблема синхронизации все равно не возникает. Конечным результатом является то, что SqlException генерируется в одном из потоков после того, как SQL обнаруживает тупик и завершает один из операторов. Я могу поймать это исключение и обнаружить конкретный код ошибки, но он не очень чистый.

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

Ответы [ 4 ]

2 голосов
/ 28 апреля 2009

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

Уровень изоляции "сериализуемый" должен фактически работать. Он не позволяет изменять данные, прочитанные из одной транзакции, другой. Но он сильно блокируется и, как правило, его не следует использовать, поскольку он замедляет работу приложения и повышает риск возникновения мертвых блокировок.

Альтернативы:

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

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

0 голосов
/ 29 апреля 2009

(на самом деле разрешено иметь больше чем одна строка для виджета, но демон никогда не должен этого делать автоматически)

Тогда проблема не в базе данных. это тот факт, что у вас есть несколько демонов, которые не знают, что обнаружили один и тот же объект. Мне кажется, что именно на этом вам нужно сосредоточить свое внимание. С точки зрения базы данных, они оба представляют совершенно допустимые строки.

Что произойдет, если вы измените:

1: every so often:
2:   find newest N widgets (from external source)
3:   foreach widget
4:     if( widget not yet in database )
5:       add rows for widget

до

1: every so often:
2:   while there are unhandled widgets
2:       find first unhandled widget
4:       if( widget not yet in database )
5:           add one row for widget
0 голосов
/ 28 апреля 2009

Если у вас есть SQL Server 2008 (или другая СУБД, которая его поддерживает), рассмотрите возможность использования оператора MERGE. Это можно записать для вставки, только если строка не существует.

0 голосов
/ 28 апреля 2009

Как проверить, «если (виджет еще не в базе данных)». Если это написано в sql в форме «Select», вы можете использовать «Select for update», чтобы позволить только одному экземпляру демона делать это одновременно. Используя транзакцию для двух последних строк и используя «выбрать для обновления» для блокировки, вы избежите гонки.

Я уверен, что в ado.net есть эквивалент.

...