Содержание
- Deadlock в insert запросе в postgres
- Ответы (1 шт):
- Как мы ловим Deadlock`и на PostgreSQL и чиним их
- Предисловие
- Немного о внутреннем устройстве бекенда
- Часть 1: Мониторинг
- Как проявляется Deadlock
- Ручной захват
- application_name
- Думаем о серверных логах
- Часть 2: Как бороться с дедлоками
- Несколько практик избегания deadlock`ов
- Частый случай №1: Классический дедлок
- Частый случай №2: Сам себе злобный буратино (ССЗБ)
- Бонус №1
- Бонус №2
- PostgreSQL: Understanding deadlocks
- How does a deadlock happen?
- How to fix and avoid deadlocks
- Finally …
Deadlock в insert запросе в postgres
Получаю время от времени ошибку
Message: SQLSTATE[40P01]: Deadlock detected: 7 ERROR: deadlock detected
Запрос вида (для примера сокращу, в запросе может быть до 250 строк и много контента):
Так же есть ньюанс что подобные запросы выполняются на 40 серверах к одной базе данных
Как с таким бороться?
Ответы (1 шт):
в запросе может быть до 250 строк
Логично предположить, что конкурентно вставляете одни и те же данные в разном порядке.
Как же ON CONFLICT DO NOTHING ? А что он может?
- t1: begin
- t2: begin
- t1: insert 1 on conflict — никто не мешает, записали
- t2: insert 2 on conflict — никто не мешает, записали
- t2: insert 1 on conflict — хм, это же значение пишет t1, но мы не знаем можем ли мы сделать do nothing, т.к. если t1 сделает rollback — мы сделаем неправильный выбор. Потому ждём окончание t1
- t1: insert 2 on conflict — а это значение хочет t2 писать, подождём её завершения.
- t1 или t2 спустя время deadlock_timeout — что-то я долго жду, проверю-ка граф ожиданий. Опа, действительно deadlock, отменяю свою транзакцию. Вторая транзакция соответственно разблокируется.
insert на много строк всё равно транзакция и потому может быть аналогично.
Отсортируйте записываемые множества по какому-либо признаку в идентичном порядке.
Источник
Как мы ловим Deadlock`и на PostgreSQL и чиним их
Предисловие
Ситуация: есть высоконагруженная мета-игра для наших танков под названием Глобальная карта. Эдакая пошаговая настолка для команд, где бои происходят в реальном танковом клиенте. В пиковые часы на карте несколько тысяч руководителей кланов производят игровые действия: атакуют друг друга, перемещают дивизии, покупают, продают, грабят корованы. Помимо этого, существует десяток сервисов, которые также могут вносить изменения в игровую ситуацию: подкидывают деньжат, штрафуют, добавляют игроков в клан и прочее.
Всё это неизбежно приводит к дедлокам. Так вот, хочу вам поведать историю о том, как мы эти периодические проблемы держим в допустимых рамках.
Немного о внутреннем устройстве бекенда
Часть 1: Мониторинг
Как проявляется Deadlock
Когда у нас возникает Deadlock, то падает исключение следующего вида:
ERROR: deadlock detected
DETAIL: Process 18293 waits for ShareLock on transaction 639; blocked by process 18254.
Process 18254 waits for ShareLock on transaction 640; blocked by process 18293.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,9) in relation «users»
Первое, на что следует обратить внимание, — это строчка:
HINT: See server log for query details.
Действительно, если мы посмотрим серверные логи, то увидим для этого же места следующее:
ERROR: deadlock detected
И дальше конкретику:
DETAIL: Process 18293 waits for ShareLock on transaction 639; blocked by process 18254.
Process 18254 waits for ShareLock on transaction 640; blocked by process 18293.
Process 18293: update users set balance = balance + 10 where > Process 18254: update users set balance = balance + 10 where >
HINT: See server log for query details.
CONTEXT: while updating tuple (0,9) in relation «users»
И, наконец, запрос, на котором произошла ошибка:
STATEMENT: update users set balance = balance + 10 where >
![]()
Логирование запросов при этом не обязано быть включено.
Круто. Но первая глобальная проблема для любого более-менее серьёзного проекта — то, что у вас нет доступа к серверным логам вследствие политики безопасности. Иногда вообще нет никакого доступа. А иногда можно попросить участок, но надо ждать. Иногда это 30 минут, иногда день.
А хотелось бы получать такую информацию сразу. В особенности, если у вас в проекте есть Sentry, и большинство ошибок команда разработки получает сразу.
Как-то подкрутить сервер, чтобы он такую информацию выдавал обычным клиентам — нельзя. Вследствие политики безопасности разработчиков базы. Но, если у вашего пользователя к базе доступ обычный, без всяких там ограничений на выполнения служебных функций и без Row-Level security policies, то организовать себе доступ к подобной информации всё же можно.
Ручной захват
Мы можем преобразовать наши классы так, чтобы вручную получать похожую информацию. И даже больше. Для этого после отлова исключения о дедлоке, нам необходимо:
- Временно ничего не откатывать в текущем соединении с базой и вообще ничего не трогать там.
- Создать ещё одно соединение и выполнить в нём простейший запрос:
В данном случае у нас есть высокая вероятность того, что мы увидим какой именно запрос сломал нам транзакцию, вычислив его по PID и посмотрев текущий query.
Но бывает и так, что вычислив соединение по PID и посмотрев на query вы можете увидеть совсем не тот query, который устроил нам дедлок, а какой-нибудь следующий за ним по логике. Ведь пока вы ловили исключение и открывали соединение, нужный нам запрос для отлова мог и завершиться. Всё что мы можем здесь сделать — это работать через pgBouncer или его аналоги для минимизации времени установления соединения и использовать application_name.
application_name
Даже если вы получили тот запрос, который вызвал дедлок, у вас всё равно могут возникнуть трудности с пониманием, в каком месте логики он был вызван. И вот здесь на помощь приходит поле application_name. По умолчанию оно инициализируется не сильно полезной информацией, но его можно менять. А что если писать туда то место, откуда мы начинали транзакцию?
Вуаля. Теперь можно быстро открывать файлы в нужных местах и смотреть код.
pid | application_name | state | query | |
---|---|---|---|---|
1 | 8613 | deadlock_test.py:10 | idle in transaction (aborted) | UPDATE users SET balance = balance + 10 WHERE RETURNING pg_sleep(1); |
2 | 8614 | deadlock_test.py:17 | active | UPDATE users SET balance = balance + 10 WHERE RETURNING pg_sleep(1); |
3 | 8617 | active | SELECT pid, application_name, state, query FROM pg_stat_activity; |
Думаем о серверных логах
Вся эта магия, описанная сверху хороша, но теоретически может не сработать. Иногда вам всё же не обойтись без серверных логов, поэтому необходимо сделать ещё два шага:
- Обговорить понятную процедуру получения нужных участков серверных логов в разумное время с заинтересованными сторонами.
- Делать их в требуемом вами формате, изменив log_line_prefix в postgresql.conf. На машине разработчика например можно так: log_line_prefix = ‘APP:%a PID:%p TR:%x ‘ .
Часть 2: Как бороться с дедлоками
Обычно в таких разделах дают ссылки на документацию об уровнях изоляции транзакций, видах блокировок и предлагают думать и анализировать в разрезе своего приложения. Я тоже так сделаю, но позже. А сначала просто опишу как мы это делаем чаще всего, ибо так уже получилось, что дедлоки у нас очень похожи друг на друга.
Несколько практик избегания deadlock`ов
Частый случай №1: Классический дедлок
Самый наш частый случай следующий:
- Происходит игровое действие, затрагивающее сразу несколько игроков.
- При этом игроки нередко могут совершать несколько действий в одно и то же время. Например, у нас: у крупных кланов есть много подразделений, участвующих в боях.
- При выигрыше или проигрыше начисляются очки клану. И таких начислений параллельно может быть очень много.
То есть имеем ситуацию, когда пришёл пяток результатов боёв, их обработка распараллелилась, при этом в параллельных процессах, бывает, встречаются одни и те же игроки.
Всё что тут можно сделать: или выстраивать начисления в цепочку, но это медленно, или позволять начислениям падать и пробовать начислить чуть позже.
Частый случай №2: Сам себе злобный буратино (ССЗБ)
У нас походовая игра. Раз в ход происходит пересчёт баланса игроков, учитывая большое количество совершённых ими игровых действий. На время изменения баланса мы блокировали другие изменения через SELECT… FOR UPDATE. Хотя мы блокировали не сразу всех, а чанками по 100, всё равно иногда уходили в дедлок с процессом, который начисляет бонусы за бой, который не останавливается на время расчёта хода.
Так вот, оказалось, что мы были неправы. SELECT… FOR UPDATE — слишком мощная блокировка, необходимая только если выполняются 2 условия:
- Условный id текущей таблицы используется как внешний ключ в другой.
- Этот же условный id может быть изменён/удалён в результате дальнейших действий.
Возьмём пример:
P2 в данной ситуации повиснет, поскольку мы даём СУБД понять, что запись с может перестать существовать. Однако в P1 мы не делаем ничего такого, только хотим защитить баланс клана от изменений. Поэтому, когда мы изменили FOR UPDATE на FOR NO KEY UPDATE, мы перестали ловить дедлоки.
Бонус №1
SELECT… FOR UPDATE в примере выше вызван явно. Но вы получите аналогичный эффект, если затронете своими изменениями уникальный ключ, на который ссылается внешний ключ из других таблиц. А любой UPDATE, который не затрагивает своими изменениями подобные ключи, вызовет блокировку аналогичную SELECT… FOR NO KEY UPDATE. Я вам рекомендую ознакомиться с этими особенностями в статье «Явные блокировки» в списке литературы ниже.
Бонус №2
Вернёмся к ещё одной любопытной детали из первоначальной ошибки:
CONTEXT: while updating tuple (0,9) in relation «users»
Что за тупл спросите вы? Это физический адрес строчки в таблице, из-за которой возник конфликт. Дело в том, что в каждой таблице есть служебные поля, которые запросом SELECT * не выбираются. Однако стоит явно указать к примеру ctid среди полей, как мы увидим этот самый тупл:
Пользы от него немного в случае дедлока, ибо разблокированный процесс скорее всего обновит конфликтную строчку, и у неё изменится этот ctid (поскольку любой UPDATE в PostgreSQL на самом деле INSERT, а старая строчка помечается как невидимая и позже будет удалена автовакуумом). Но знать стоит, вдруг когда-нибудь пригодится.
Источник
PostgreSQL: Understanding deadlocks
Many might have seen PostgreSQL issue the following error message: «ERROR: deadlock detected» . But what does it really mean? How can we prevent a deadlock and how can we reproduce the problem? Let’s dive into PostgreSQL locking and understand what deadlock and deadlock_timeout really mean.
How does a deadlock happen?
Many people approach us because they want to understand what a deadlock is and how it can happen. They also want to understand how a deadlock can be avoided and what software developers can do about it.
If you want to understand how a deadlock occurs, all you need is a table containing two lines. That’s sufficient to explain the basic principle of deadlocks.
Here is some easy-to-use sample data:
The crux is that if data is updated in a different order, transactions might have to wait for one another to be finished. It is perfectly fine if transaction 1 has to wait for transaction 2. But what happens if transaction 1 has to wait for transaction 2 and transaction 2 has to wait for transaction 1? In that case, the system has two choices:
- Wait infinitely, or
- Abort one transaction and commit the other transaction.
As waiting infinitely is not an option, PostgreSQL will abort one of these transactions after some time (deadlock_timeout). Here is what happens:
Transaction 1 | Transaction 2 | Comment |
BEGIN; | BEGIN; | |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+—— 1 | 1000 (1 row) |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+—— 2 | 2000 (1 row) |
works perfectly |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; |
has to wait until transaction 2 releases the lock on the row containing > | |
… waits … | UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; |
wants to lock the row locked by transaction id: now both are supposed to wait |
… deadlock timeout … | … deadlock timeout … | PostgreSQL waits (deadlock_timeout) and triggers deadlock detection after this timeout (not immediately) |
update proceeds: “UPDATE 1” | ERROR: deadlock detected | a transaction has to die |
COMMIT; | the rest commits normally |
The error message we will see is:
ERROR: deadlock detected
DETAIL: Process 70725 waits for ShareLock on transaction 891717; blocked by process 70713.
Process 70713 waits for ShareLock on transaction 891718; blocked by process 70725.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,1) in relation «t_data»
The reason is that transactions have to wait for one another. If two transactions are in a conflict, PostgreSQL will not resolve the problem immediately, rather it will wait for deadlock_timeout and then trigger the deadlock detection algorithm to resolve the problem.
Why does PostgreSQL wait for some time before it steps in and fixes things? The reason is that deadlock detection is quite expensive, and therefore not immediately triggering it makes sense. The default value here is 1 second, which is high enough to avoid pointless deadlock detection attempts, but is still short enough to fix the problem in a useful and timely manner.
How to fix and avoid deadlocks
The most important thing to know is: There is NO MAGIC CONFIGURATION PARAMETER to fix this problem. The problem does NOT depend on configuration. It depends on the execution order of operations. In other words, you cannot magically fix it without understanding the application and its underlying operations.
The only thing that can fix the problem is to change the execution order, as shown in the next listing:
This is the data you should see after committing the transaction that did not fail before. Thus we can see what happens if two transactions execute in a different order:
Transaction 1 | Transaction 2 | Comment |
BEGIN; | ||
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+—— 1 | 1000 (1 row) |
BEGIN; | |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; |
||
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+——- 2 | 20000 (1 row) |
… wait … | |
COMMIT; | … wait … | |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+——— 1 | 100000 (1 row) |
re-read the value and use the newly committed entries | |
UPDATE t_data SET data = data * 10 WHERE/> RETURNING *; id | data —-+——— 2 | 200000 (1 row) |
re-read the value and use the newly committed entries | |
COMMIT; |
In this case, there is no deadlock. However, in a real work scenario it is hardly possible to simply swap the execution order. That’s why this is more of a theoretical solution to the problem than a practical one. However, there are no other options to fix the problem of deadlocks. In the case of deadlocks, being aware of how to prevent them is the best cure.
Finally …
Locking is really important. Deadlocks are not the only concern in this area. Performance might be equally important, therefore it makes sense to deal with performance-related locking effects as well. Stay tuned for more on this topic.
If you want to learn more about important features of PostgreSQL, you might want to check out a blog post about UPDATE which can be found here.
Please leave your comments below. In order to receive regular updates on important changes in PostgreSQL, subscribe to our newsletter , or follow us on Twitter , Facebook , or LinkedIn .
Hans-Jürgen Schönig
Hans-Jürgen Schönig has experience with PostgreSQL since the 90s. He is CEO and technical lead of CYBERTEC, which is one of the market leaders in this field and has served countless customers around the globe since the year 2000.
Источник
Мое приложение массово получает JMSTextMessages от IBM Mq. Для каждого сообщения я вызываю REST API для обработки этих сообщений. Где-то, когда API обрабатывает эти сообщения, я периодически, но очень часто, получаю следующие журналы исключений:
java.lang.Exception: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
2018-04-03 17:54:10.614 ERROR 5259 --- [.0-8083-exec-59] o.h.engine.jdbc.spi.SqlExceptionHelper : ORA-00060: deadlock detected while waiting for resource
Он не печатает ни таблицу, ни запрос, который пытается выполнить. Чтобы решить эту проблему, я попытался протолкнуть весь код обработки базы данных в код службы REST API внутри синхронизированного блока, но проблема все еще та же. Из-за этой проблемы не удается обработать каждое второе сообщение.
Основываясь на большом количестве материалов, доступных в Интернете, кажется, что каждое полученное сообщение запускает новый поток (в конце метода службы REST API) и, таким образом, вызывает какое-то тупиковое поведение в моем приложении. К сожалению, я не могу понять, какой именно фрагмент кода вызывает это, и у нас есть один поток, когда дело доходит до запуска приложения-службы.
Итак, теперь я решил выяснить, могу ли я ввести задержку, прежде чем я начну обрабатывать другое сообщение на стороне слушателя. Снова много читаю, и везде я вижу изменения свойств XML, чтобы справиться с этим, но ничего не пригодится, если я хочу сделать это через Spring Annotations. В моем приложении нет конфигураций XML.
Вот мой псевдокод:
@JmsListener(destination = "queue_name")
public void receiveMessage(Object message){
myRestService.processMessage(message);
}
Код ServiceImpl
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processMessage(Object message){
Long id = getIdFromMessage(message);
X x = readFromDB(id);
updateDataInX(x, message);
savex(x);
}
Я также хотел бы подчеркнуть, что у меня есть родительско-дочерние отношения в базе данных. Где PRIMARY_KEY родительской таблицы — это PRIMARY_KEY (а также FK) дочерней таблицы, и она также индексируется. Пока я обновляю значения в указанном выше методе (updateDataInX), некоторые столбцы обновляются в строке дочерней таблицы, а некоторые столбцы обновляются в строке родительской таблицы (сопоставляются с дочерней строкой). Экземпляр x принадлежит к классу CHILD.
Как я могу это исправить, помимо введения задержки? Производитель — это отдельное приложение, и в течение секунды они отправляют несколько сообщений, которые потребитель явно не может обработать. Это полностью внутреннее обновление, поэтому я не возражаю ввести задержку, чтобы иметь отказоустойчивый механизм, но я не нашел подходящего решения с аннотациями Spring. Также, если вы можете сказать, что именно происходит с потоками на стороне получателя и на стороне API отдыха. Я полностью сбит с толку, читая об этом последние 3 недели.
My overall goal is to get a 40P01/ERRCODE_T_R_DEADLOCK_DETECTED
from postgres when an deadlock is detected. (To be more precise, I would like to get a LockAcquisitionException in Hibernate, which is the exception the 40P01/ERRCODE_T_R_DEADLOCK_DETECTED
error code is translated in PostgreSQL81Dialect.java) I use Postgresql 9.6.
For this I thought I should set the deadlock_timeout
and log_lock_waits
config variables
as suggested by 19.12. Lock Management
deadlock_timeout
(integer) This is the amount of time, in
milliseconds, to wait on a lock before checking to see if there is a
deadlock condition.
[…]
Whenlog_lock_waits
is set, this parameter also determines the length
of time to wait before a log message is issued about the lock wait. If
you are trying to investigate locking delays you might want to set a
shorter than normaldeadlock_timeout
.
I’ve set the following values in postgresql.conf
log_lock_waits = on # log lock waits >= deadlock_timeout
deadlock_timeout = 5s
Now, when I create a deadlock situtation (from Java using Hibernate) I find the following in the postgresql.log
LOG: process 17493 still waiting for ShareLock on transaction 322815 after 5000.464 ms
DETAIL: Process holding the lock: 17495. Wait queue: 17493.
CONTEXT: while updating tuple (132,35) in relation "publication"
However no 40P01/ERRCODE_T_R_DEADLOCK_DETECTED
error is generated (and sent to the JDBC driver).
I digged a little but into the postgres source code and found that the dead lock detection done by setting deadlock_timeout/log_lock_waits
is a different mechanism than the one generating 40P01/ERRCODE_T_R_DEADLOCK_DETECTED
. The deadlock_timeout
case is handled in backend/storage/lmgr/proc.c
, while the 40P01/ERRCODE_T_R_DEADLOCK_DETECTED
case in backend/storage/lmgr/deadlock.c
So, my questions are:
- Are these actually two different types of deadlocks that are detected?
- Is there a way to get an error when
deadlock_timeout
based deadlock detection happens? - How can actually a
ERRCODE_T_R_DEADLOCK_DETECTED
error be forced to happen?
UPDATE: the code I use to get into the deadlock situation goes like this (Spring/Java):
// This is in a Transaction managed by spring
Publication p = em.find(Publication.class, id);
p.setComment("Outer "+new Date());
em.flush();
// This utility method runs the lambda expression in a new Transaction
// using Spring's TransactionTemplate and tries to update
// the same Publication that is about to be updated by the
// "outer" transaction
Utils.runInSeparateTransaction(status -> {
Publication p2 = em.find(p.getClass(), p.getMtid());
p2.setComment("Inner "+new Date());
return p2;
// Would flush/commit after the return, but will "hang" instead.
// Here I would expect the deadlock error but only get the
// deadlock log.
};
// Outer transaction would commit at this point but will