Pull to refresh

Comments 22

А я думал, что в статье напишите "Вешайте TxMode.NEVER на все клиенты к внешним системам" :)

Тоже своего рода защита пула коннектов от исчерпания из-за медленных зависимостей...

Альтернативное решение — использовать аннотацию @Transactional на методе.

За такое надо бить по рукам железной линейкой. Можно даже ребром. Никогда, никогда репозиторный метод не должен заниматься транзакциями.

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

Самое печальное - транзакции на репозиториях сделаны из коробки в spring-data, но, вместо логичного @Transactional(propagation = MANDATORY), там простой @Transactional.

Так а что делать если не 3 слоя а два, без фасадов?

Очевидно, что нужно их добавить, нет?

Тут ещё дело такое: то, что вы фасады не создаёте, не означает, что их нет. Просто они генерируются тем же спрингом: бизнес-логика - сервис, написанный руками и проаннотированный всякими @Transactional и иже с ним, инфраструктурный фасад - реальный класс бина во время исполнения кода, после наворачивания всей магии АОП.

Если сервисы бизнес-логики не пересекаются и не переиспользуются друг внутри друга, то аннотировать их @Transactional допустимо. Но надо точно понимать, что происходит во время исполнения кода.

Грубо говоря, правило такое: в стеке вызовов @Transactional-метод должен гарантированно встречаться лишь единожды. При несоблюдении сего простого правила, как минимум, появляется код, который лишь отапливает помещение серверной, проверяя перед вызовом методов, а открыта ли транзакция.

А понимание этого приходит сразу после того, как отключишь АОП и попробуешь руками, например, на TransactionTemplate написать всё то, что будет сгенерировано в прокси-классах.

Опять вредные советы подъехали из разряда: ставьте везде аннотацию Transactional и будет вам хорошо.

По моему вы недостаточно разобрались в API для вызова процедур с которым работаете.

Даже если посмотреть строгу по коду, не читая javadoc, то уже видно что:

  • сначала вы выполняете запрос query.execute();

  • далее извлекаете результат query.getOutputParameterValue();

После этого становиться очевидно, что метод execute создает внутри какой-то стейт(курсор) из которого уже далее извлекаются результаты. Поэтому напрашивается вопрос, а закрывать этот ресурс кто будет?) Вы не закрыли ресурс, получили проблему... извините но ни обработка ошибок, ни Transactional тут не причем.

После чтения javadoc все становится более понятным. Метод execute по завершению возвращает флаг есть ли результат или нет. А далее уже идет работа с результатом/курсором - поэтому коннект и удерживается. Но все же справедливости ради: данное API весьма запутанное. Гугление по "storedprocedurequery transaction", выдало вот такой вот результат - совсем не очевидно как же тут закрыть соединение средствами API.
На одном из прошлых мест работ была похожая ситуация. Нужно было импортировать в PostgreSQL csv файлы. В качестве тулзы для работы с БД использовали Spring JdbcTemplate. Разраб что делал задачу, нашел что в драйвере JDBC PostreSQL есть соответсвующий класс CopyManager, который на вход принимает коннект. Найдено - сделано. Заинжектил DataSource, получил из него коннект, сделал свое дело и пошел дальше) То что коннект надо освобождать он не подумал, в итоге получили аналогичную проблему. Хотя надо было просто воспользоваться соответствующим методом из JdbcTemplate. Но соглашусь что у вас случай более запутанный, т.к. есть открытое issue по поводу закрытия ресурсов - в интерфейсе специальных методов для этого нет.

Мораль все же в том - что надо лучше понимать что делаете, читать javadoc и гуглить проблемы) А вешать транзакции где попало, это не выход, хотя в Вашем случае это действительно решает проблему, т.к. Spring сам закроет транзакцию и как следствие курсор с данными от хранимки. Особенно так вредно делать с одиночными запросами к бд. Писал по этому поводу статью - бесплатно в этой жизни ничего не бывает и за все приходиться чем-то платить.

А, кстати, я правильно предполагаю, что entityManager.createStoredProcedureQuery вне транзакции не работает? Т.е. границы транзакции всё таки были где-то "выше"?

Но транзакция не закрывалась, потому что наружу выходило checked exception EDeliveryException?

И проблема решилась не столько добавлением @Transactional к createInboxMessage методу — это какбэ принципиально не верно — задавать границы транзакции в слое репозитория, а потому что перестали checked exception бросать

} catch (PersistenceException ex) {
    logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
}

...?

Глушить эксепшены тоже, кстати, нехорошая практика.

В общем, вопросы есть к изложенному в статье.

Ответ есть выше - https://habr.com/ru/companies/spring_aio/articles/827642/comments/#comment_27025050

Если кратко то StoredProcedureQuery имеет стейт, который надо "закрывать". Но апи для работы с ним очень кривое и там нет явно метода для этого. На это тему есть открытое issue + workaround с явным кастом к ProcedureCall и далее ProcedureCall#getOutputs().release()

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

Не понял каким образом здесь помогла явная аннотация. Транзакция открывалась же где-то и без неё, скоро всего ниже по стеку, и там же и закрывалась. Теперь старт транзакции был перенесён выше

Действительно, это не раскрыто в оригинальной статье. Можно предположить, что там стоял open-in-view=true, что вообще скорее антипаттерн

Битриксом пахнуло... Я устал считать количество параметров у метода.

Довольно вредный совет, нужно быть очень осторожным, такая транзакция может не открыться в случае если метод помеченный такой аннотацией вызывается из другого метода этого же класса, то есть через this – такой вызов не проходит через Proxy который создаётся спрингом на старте приложения для бина этого класса, и соответственно транзакция не открывается.

Просто при установке аннотации таймаут транзакции отрабатывает корректно.

В Spring реализация менеждера транзакций достаточно кривая. Несколько лет назад столкнулись с неприятнейшей ошибкой, с которой, скорей всего, мало кто столкнется, но пришлось залезть глубоко в реализацию.

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

Пришлось принудительно управлять второй транзакцией в собственном менеджере транзакций с привязкой через ThreadLocal.

Недавно посмотрел код - ничего не поменялось :(

Вы можете подтвердить баг тестовым проектом, демонстрирующем проблему? Отправили баг-репорт в Spring?

Атрибут transactionManager в `@Transactional` конечно же использовали?

Конечно, все было сделано. Но, именно, ввиду корявости кода в том месте в некоторых случаях возникали ошибки.

В одном месте кода был комментарий: "Если вы сюда попали, то делаете что-то уникальное. Я не смог найти решение. Поделитесь, пожалуйста"

В другом (в Spring Cloud): "Если это вам нужно, то сделайте, пожалуйста, сами".

"Не боги горшки обжигают"(с)

Крайне интересно. Поделитесь техническими подробностями, пожалуйста.

Можете показать код, воспроизводящий баг? Который в баг-репорт прикладывали?

И вот эти комментарии

Если вы сюда попали, то делаете что-то уникальное

и

Если это вам нужно, то сделайте, пожалуйста, сами

... они в каких конкретно проектах/файлах/строчках?

Предполагаю, что вы ещё в контексте проблемы, раз

Недавно посмотрел код - ничего не поменялось :(

Вообщето Spring Transactional Manager использует именно ThreadLocal для хранения метаинформации о состоянии транзакции и соединения. Подробнее тут и тут.

Скорее всего вы делали что то не так. Ну и фраза

используется два соединения к базе

Звучит очень странно. Если вы переливаете данные из одной БД в другую или в рамках разных схем одной дб, то соединения должны быть получены из разных Transactional Manager о чем уже тут писали и это надо указывать явно в аннотации транзакции.

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

Спрашивается сколько будет по итогу открыто транзакций для 1 и для 2 случая?

    @Transactional //1
    public void update() {
        IntStream.range(0, 100).forEach(i -> updateDb(i));
    }

    @Transactional //2
    public void update() {
        IntStream.range(0, 100).parallel().forEach(i -> updateDb(i));
    }
    
    private void updateDb(int index) {
        //perform row update by index
    }

Правильный ответ: в первом случае будет одна транзакция. А во втором 101 т.к. действия по апдейту будут выполнятся в других потоках и у них в ThreadLocal не будет данных о том что транзакция открыта.

<занудамод> не в 101 а все же в том сколько будет потоков у ForkJoinPool, можно сделать и чтобы было 101, но все же обычно без кастомизации будет количество ядер </занудамод>

Да, до моей формулировки можно докопаться, но и вы не правы. Если сделать уточнение - что метод update вызывается не из форк джоин пула, то количество транзакций всегда будет 101, независимо от того сколько в нем потоков. Каждый вызов updateDbбудет выполнен в новой транзакции. И даже если на одном потоке будет выполенено скажем 10 вызовов updateDbто по итогу будет 10 транзакций в БД. Т.к. каждый запрос сам откроет и закроет ее по завершению на стороне БД.

Я был в твёрдой уверенности, что Spring работает через proxy-обёртки (оставим сейчас в стороне CGLIB).

Соответственно, при вызове второго метода, транзакция будет создана в обёртке ровно один раз.

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

В каком месте и как тут создаётся ещё 100 транзакций? Сам метод update создаёт транзакцию, если её нет в threadLocal? Не встречал такого, можно подробней?

В каком месте и как тут создаётся ещё 100 транзакций? Сам метод update создаёт транзакцию, если её нет в threadLocal? Не встречал такого, можно подробней?

Возможно это будет открытием, но именно так и происходит. Если выполнить какую нибудь операцию к БД, и выше по стеку нигде не будет открытой транзакции, то будет выполнен запрос к бд с параметром autocommit=true(true дефолтное значение). Это как бы база… (Более подробно я рассказывал про autocommit  в своей статье про транзакции). Так будет на чистом JDBC и springJdbcTemplate. Другие технологии где куча специфики и неявной логики вроде JPA я в расчет не беру, разговор не про них. И если вы явно не указали транзакцию, то БД так или иначе сама ее откроет в режиме autocommit - по другому никак.

Поэтому дальше, по логике, updateDb либо свалится с ошибкой, из-за отсутствия транзакции, либо не запишет ничего в базу.

Это опять не соответствует действительности. Откуда такая информация? Выше я уже ответил также и на это. По вашей логике нельзя выполнить никакой запрос к бд, не поставив аннотацию Transactional? Но это не так) Далее я докажу вам это.

Транзакции я считал на стороне БД. Давайте рассмотрим еще раз на примере. Создал методы как писал выше. Полный код.Также добавил метод currentTxId который возвращает текущий номер транзакции на стороне БД (Более подробно про это тут.)

    @Transactional
    fun update10TxSequence() {
        IntStream.range(0, 10).forEach { i: Int -> insert(i.toString()) }
    }

    @Transactional
    fun update10TxParallel() {
        IntStream.range(0, 10).parallel().forEach { i: Int -> insert(i.toString()) }
    }

    fun insert(isbn: String): Int {
        return jdbcTemplate.update("INSERT INTO books (name, isbn) VALUES (?, ?)", isbn, isbn)
    }

    fun currentTxId(): MutableList<MutableMap<String, Any>> {
        return jdbcTemplate.queryForList("SELECT txid_current()")
    }

Первый тест с последовательным выполнением, результат:

    @Test
    fun testTxSequence() {
        println("count before: " + bookRepository.count())
        println("tx_id before: " + bookRepository.currentTxId())
        bookRepository.update10TxSequence()
        println("tx_id after: " + bookRepository.currentTxId())
        println("count after: " + bookRepository.count())
    }

    //count before: {count=0}
    //tx_id before: [{txid_current=504}]
    //tx_id after: [{txid_current=506}]
    //count after: {count=10}

Откуда видно что была ровно одна транзакция на стороне БД. Небольшое уточнение - каждый вызов currentTxId также делает инкремент транзакции на стороне бд. Поэтому надо учитывать его вызов. Показал это на тесте - testTxIdIncrement. Каждый вызов insert видит что в threadLocal есть инфо о открытой транзакции, и он просто переиспользует ее и не создает новую.

Второй тест с параллельным выполнением, результат:

    @Test
    fun testTxParallel() {
        println("count before: " + bookRepository.count())
        println("tx_id before: " + bookRepository.currentTxId())
        bookRepository.update10TxParallel()
        println("tx_id after: " + bookRepository.currentTxId())
        println("count after: " + bookRepository.count())
    }

    //count before: {count=0}
    //tx_id before: [{txid_current=491}]
    //tx_id after: [{txid_current=502}]
    //count after: {count=10}

Видно что было 10 транзакций. Для метода update10TxParallel транзакция не создалась на стороне БД т.к. он по факту в ней ничего не модифицировал и постгресс такие транзакции считает условно “readonly” и счетчик для них не модифицирует. Тут каждый вызов insert ничего не находит в threadLocal и выполняет транзакцию с новым коннектом и параметром autocommit=true, что создает новую транзакцию в бд.

*Я заменил update на insert чтобы не заполнять бд значениями. Т.к. если выполнить апдейт для несуществующей строки, то транзакция также не будет учтена т.к. ничего не поменяла. Мне было лень заполнять бд значениями. Но вы можете это протестировать сами на моем примере.

Если используется два разных физических соединения к базам, то использование разных менеджеров транзакций очевидно. Ну, и про многопоточность и конкурентную работу тоже было указано. Так что школьные задачки, с дурно пахнущим кодом, можно было не приводить. А то, ненароком, кто-то ещё использует ;)

Sign up to leave a comment.