Postgres 9.3: проблема Sharelock с простой вставкой
Обновление: потенциальное решение ниже
У меня есть большой набор файлов конфигурации, состоящий из пар ключ / значение, которые я пытаюсь вставить в базу данных. Многие ключи и значения повторяются в файлах конфигурации, поэтому я храню данные, используя 3 таблицы. Один для всех уникальных значений ключей, один для всех уникальных значений пар и один список всех пар ключ / значение для каждого файла.
Проблема: я использую несколько параллельных процессов (и, следовательно, соединений), чтобы добавить необработанные данные в базу данных. К сожалению, я получаю много обнаруженных взаимоблокировок при попытке добавить значения в таблицы ключей и значений. Я пробовал несколько разных способов вставки данных (показано ниже), но всегда заканчивался ошибкой "обнаружена взаимоблокировка"
TransactionRollbackError: обнаружена взаимоблокировка
ДЕТАЛИ: Процесс 26755 ожидает ShareLock для транзакции 689456; заблокирован процессом 26754. Процесс 26754 ожидает ShareLock для транзакции 689467; заблокирован процессом 26755.
Мне было интересно, может ли кто-нибудь пролить некоторый свет на то, что именно может вызвать эти тупики, и, возможно, указать мне на какой-то способ решения проблемы. Глядя на операторы SQL, которые я использую (перечислены ниже), я не понимаю, почему вообще существует какая-либо взаимозависимость.
Спасибо за прочтение!
Пример файла конфигурации:
example_key this_is_the_value
other_example other_value
third example yet_another_value
Табличные определения:
CREATE TABLE keys (
id SERIAL PRIMARY KEY,
hash UUID UNIQUE NOT NULL,
key TEXT);
CREATE TABLE values (
id SERIAL PRIMARY KEY,
hash UUID UNIQUE NOT NULL,
key TEXT);
CREATE TABLE keyvalue_pairs (
id SERIAL PRIMARY KEY,
file_id INTEGER REFERENCES filenames,
key_id INTEGER REFERENCES keys,
value_id INTEGER REFERENCES values);
Заявления SQL:
Первоначально я пытался использовать это утверждение, чтобы избежать каких-либо исключений:
WITH s AS (
SELECT id, hash, key FROM keys
WHERE hash = 'hash_value';
), i AS (
INSERT INTO keys (hash, key)
SELECT 'hash_value', 'key_value'
WHERE NOT EXISTS (SELECT 1 FROM s)
returning id, hash, key
)
SELECT id, hash, key FROM i
UNION ALL
SELECT id, hash, key FROM s;
Но даже что-то простое приводит к тупикам:
INSERT INTO keys (hash, key)
VALUES ('hash_value', 'key_value')
RETURNING id;
- В обоих случаях, если я получаю исключение, потому что вставленное значение хеша не является уникальным, я использую точки сохранения, чтобы откатить изменение, и другой оператор, чтобы просто выбрать идентификатор, который я ищу.
- Я использую хеши для уникального поля, так как некоторые ключи и значения слишком длинные для индексации
Полный пример кода Python (с использованием psycopg2) с точками сохранения:
key_value = 'this_key'
hash_val = generate_uuid(value)
try:
cursor.execute(
'''
SAVEPOINT duplicate_hash_savepoint;
INSERT INTO keys (hash, key)
VALUES (%s, %s)
RETURNING id;
'''
(hash_val, key_value)
)
result = cursor.fetchone()[0]
cursor.execute('''RELEASE SAVEPOINT duplicate_hash_savepoint''')
return result
except psycopg2.IntegrityError as e:
cursor.execute(
'''
ROLLBACK TO SAVEPOINT duplicate_hash_savepoint;
'''
)
#TODO: Should ensure that values match and this isn't just
#a hash collision
cursor.execute(
'''
SELECT id FROM keys WHERE hash=%s LIMIT 1;
'''
(hash_val,)
)
return cursor.fetchone()[0]
Обновление: так что я полагаю, что подсказка на другом сайте stackexchange:
В частности:
Команды UPDATE, DELETE, SELECT FOR UPDATE и SELECT FOR SHARE ведут себя так же, как и команды SELECT, с точки зрения поиска целевых строк: они будут находить только те целевые строки, которые были зафиксированы на момент запуска команды1. Однако такая целевая строка, возможно, уже была обновлена (или удалена, или заблокирована) другой параллельной транзакцией к моменту ее обнаружения. В этом случае потенциальный апдейтер будет ожидать первой транзакции обновления, чтобы зафиксировать или откатить (если она все еще выполняется). Если первый модуль обновления откатывается назад, то его эффекты отменяются, и второй модуль обновления может продолжить обновление первоначально найденной строки. Если первый обновитель фиксирует, второй обновитель проигнорирует строку, если первый обновитель удалил ее2, в противном случае он попытается применить свою операцию к обновленной версии строки.
Хотя я до сих пор не совсем уверен, где находится взаимозависимость, кажется, что обработка большого количества пар ключ / значение без фиксации может привести к чему-то вроде этого. Конечно же, если я фиксирую после добавления каждого отдельного файла конфигурации, взаимоблокировки не возникает.
2 ответа
Похоже, вы находитесь в этой ситуации:
- Таблица для вставки имеет первичный ключ (или уникальный индекс (ы) любого вида).
- Несколько INSERT в эту таблицу выполняются в одной транзакции (в отличие от фиксации сразу после каждой)
- Строки для вставки располагаются в случайном порядке (относительно первичного ключа)
- Строки вставляются в параллельные транзакции.
Эта ситуация создает следующую возможность для тупика:
Предполагая, что есть два сеанса, каждый из которых начал транзакцию.
- Сессия № 1: вставить строку с ПК 'A'
- Сессия № 2: вставить строку с ПК 'B'
- Сессия #1: попробуйте вставить строку с PK 'B' => Сессия # 1 помещена в ожидание, пока Сессия #2 не завершится или не произойдет откат
- Сессия #2: попробуйте вставить строку с PK 'A' => Сессия #2 помещена в ожидание Сессии #1.
Вскоре после этого детектор взаимоблокировки узнает, что оба сеанса теперь ждут друг друга, и завершает один из них с обнаруженной фатальной ошибкой взаимоблокировки.
Если вы находитесь в этом сценарии, самое простое решение - это COMMIT после вставки новой записи, прежде чем пытаться вставить любую новую строку в таблицу.
Честно говоря, Postgres известен такими тупиками. Я часто сталкиваюсь с такими проблемами, когда разные воркеры обновляют информацию о чередующихся сущностях. Недавно у меня была задача импортировать большой список метаданных научных статей из нескольких файлов json. Я использовал параллельные процессы через joblib для одновременного чтения из нескольких файлов. Тупики все время висели на
if not rexecute("sismember", "known_authors", author_id):
# your logic...
rexecute("sadd", "known_authors", author_id)
Который всем рекомендую. Используйте Memurai, если вы ограничены Windows. Печально, но факт, других вариантов у Postgres не так много.