У меня есть система, которая имеет сложный первичный ключ для взаимодействия с внешними системами, и быстрый, непрозрачный первичный ключ для внутреннего использования. Например: внешний ключ может быть составным значением - что-то вроде (имя (varchar), фамилия (varchar), почтовый индекс (char)), а внутренний ключ будет целым числом («идентификатор клиента»).
Когда я получаю входящий запрос с внешним ключом, мне нужно найти внутренний ключ - и вот сложная часть - выделить новый внутренний ключ, если у меня его еще нет для данный внешний идентификатор.
Очевидно, что если у меня есть только один клиент, говорящий с базой данных одновременно, это нормально. SELECT customer_id FROM customers WHERE given_name = 'foo' AND ...
, затем INSERT INTO customers VALUES (...)
, если я не найду значение. Но, если есть потенциально много запросов, поступающих от внешних систем одновременно, и многие из них могут прийти к ранее не услышанному клиенту сразу, есть условие состязания, когда несколько клиентов могут попытаться INSERT
новая строка.
Если бы я модифицировал существующую строку, это было бы легко; сначала просто SELECT FOR UPDATE
, чтобы получить соответствующую блокировку на уровне строк, прежде чем UPDATE
. Но в этом случае у меня нет строки, которую можно заблокировать, потому что строка еще не существует!
Я уже придумал несколько решений, но у каждого из них есть несколько довольно существенных проблем:
- Перехватите ошибку на
INSERT
, повторите всю транзакцию сверху. Это является проблемой, если в транзакции участвует десяток клиентов, особенно если во входящих данных каждый раз речь идет об одних и тех же клиентах в разном порядке. Можно застрять во взаимно рекурсивных тупиковых циклах, где конфликт возникает каждый раз с другим клиентом. Вы можете смягчить это с экспоненциальным временем ожидания между попытками повторной попытки, но это медленный и дорогой способ справиться с конфликтами. Кроме того, это немного усложняет код приложения, так как все должно быть перезапущено.
- Использовать точки сохранения. Начните точку сохранения до
SELECT
, поймайте ошибку на INSERT
, а затем откатитесь до точки сохранения и снова SELECT
. Точки сохранения не являются полностью переносимыми, а их семантика и возможности немного различаются между базами данных; самое большое различие, которое я заметил, заключается в том, что иногда они, кажется, гнездятся, а иногда нет, поэтому было бы неплохо, если бы я мог их избежать. Впрочем, это только смутное впечатление - неточно? Стандартизированы ли точки сохранения или, по крайней мере, практически соответствуют? Кроме того, точки сохранения затрудняют параллельное выполнение операций в одной и той же транзакции , потому что вы не сможете точно определить, сколько работы вы будете откатывать, хотя я понимаю, что мне просто нужно жить с этим.
- Получите некоторую глобальную блокировку, например блокировку на уровне таблицы, используя инструкцию LOCK ( oracle mysql postgres ). Это, очевидно, замедляет эти операции и приводит к большому количеству конфликтов блокировок, поэтому я предпочел бы избежать этого.
- Получите более детальную, но зависящую от базы данных блокировку. Я знаком только с способом Postgres сделать это , который совершенно точно не поддерживается в других базах данных (функции даже начинаются с "
pg_
"), так что опять же это проблема переносимости. Кроме того, способ postgres сделать это потребовал бы от меня преобразования ключа в пару целых чисел, которые он может не вписаться аккуратно. Есть ли лучший способ приобрести замки для гипотетических объектов?
Мне кажется, что это обычная проблема параллелизма с базами данных, но мне не удалось найти в ней много ресурсов; возможно, только потому, что я не знаю канонических фраз. Возможно ли сделать это с каким-то простым дополнительным битом синтаксиса в любой из тегированных баз данных?