Почему длинные транзакции плохи
Что проверяют
Понимаете ли вы цену открытой транзакции: не «долго, значит медленно», а какие именно ресурсы она удерживает, пока висит. Для java-бэкенда важнее всего связка с пулом соединений и с тем, что вы кладёте внутрь @Transactional.
Как отвечать
По слоям, от базы к приложению:
- блокировки: изменённые строки заблокированы на запись до коммита. Конкурирующие обновления выстраиваются в очередь, чаще срабатывает
lock_timeout, а вероятность взаимной блокировки растёт вместе с числом захваченных строк; - MVCC: открытая транзакция удерживает снимок, поэтому очистка не может удалить старые версии строк. В PostgreSQL это раздувание таблиц и индексов, отложенный autovacuum и растущий возраст транзакции;
- соединение: в Spring транзакция держит соединение из пула от начала и до конца. Пул невелик, и десяток долгих транзакций кладёт весь сервис на ожидании соединения, включая запросы, которым база почти не нужна;
- откат: чем длиннее транзакция, тем больше работы теряется при сбое и тем дольше восстановление.
Что предлагать: резать на короткие транзакции с идемпотентными шагами, не вызывать внешние HTTP-сервисы внутри транзакции, пакетные операции обрабатывать порциями по ключу, а конкуренцию разруливать версионированием строки вместо удержания блокировки.
Где спотыкаются
Называют грязное чтение. Оно определяется уровнем изоляции, а не длительностью: на READ COMMITTED его не будет ни в короткой транзакции, ни в часовой.
Второе: вешают @Transactional на метод, который внутри ходит в очередь или в чужой API. Транзакция теперь живёт столько, сколько отвечает соседний сервис, и его таймаут превращается в исчерпанный пул соединений у вас.
Третье: говорят «разбили на маленькие» и не говорят про согласованность. Одна транзакция давала атомарность, десять не дают ничего, поэтому нужна либо машина состояний с повторами, либо компенсирующие действия. Иначе система начнёт застревать в середине процесса.