Pull to refresh
14
0.1
Григорий Мясоедов @grisha9

Java Developer, Open Source Contributor

Send message

Также есть объективные ошибки: в книжке пишется, что в Spring 6 по дефолту используется JDK Proxy с фолбеком на CGLib. Но в Spring 5 это поведение было изменено: YouTube: Вторая чашка кофе с Joker / Евгений Борисов // 21 октября 2020.

Вы не правы. В Spring 5 маханизм проксирования не менялся, также как и в Spring 6.
Изменения есть в Spring Boot начиная с версии 2. И это поведение меняется одной настройкой в application.properties. Об этом же и говорит Борисов в своем выступлении, начиная с 36 минуты.

А зачем? Чтобы вместо БД, ходить в редис и получать по сути туже сетевую операцию, с передачей данных по сети? получается то же на то же. Я как то занимался этим и пришел к выводу, что это не имеет смысла. т.к. кешируются в основном неизменяемые справочные сущности. И сделать запрос что в бд по id, что в редис сути не меняет. Основное время займет передача данных по сети. Я даже делал замеры и результаты были одинаковые. Основная суть кеша чтобы убрать обращение к внешней системе по сети и хранить данные в памяти. А не в том чтобы заменить одну внешнюю систему на другую, концептуально ничего не улучшив и сделаться заложником доступности еще одного внешнего хранилища. К томуже современные СУДБ также имеют настраиваемый memory cache где хранят часто запрашиваемые сущности.

В каком месте и как тут создаётся ещё 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 чтобы не заполнять бд значениями. Т.к. если выполнить апдейт для несуществующей строки, то транзакция также не будет учтена т.к. ничего не поменяла. Мне было лень заполнять бд значениями. Но вы можете это протестировать сами на моем примере.

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

Вообщето 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 не будет данных о том что транзакция открыта.

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

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

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

Опять вредные советы подъехали из разряда: ставьте везде аннотацию 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 сам закроет транзакцию и как следствие курсор с данными от хранимки. Особенно так вредно делать с одиночными запросами к бд. Писал по этому поводу статью - бесплатно в этой жизни ничего не бывает и за все приходиться чем-то платить.

Получить ID события можно так - ActionManager.getInstance().getId(this)

Но по моему мнению, создавать два разных события и вешать их обработку на один класс это неправильно. В итоге внутри получаем лишние проверки. Когда можно было сразу разнести на два класса и избежать этого. Хотя если платформа такое допускает... то может все норм.

Также резануло глаз, то как вы работаете с путями и хардкодите слешь. Такое не будет работать в win. Причем самое удивительное, что далее вы пишите все верно, что в java есть соответствующая константа File.separatorChar , и что с путями правильно работать через JDK Path или через виртуальную файловую систему Virtual Files которую предоставляет платформа IDEA.

Правильно делать вот так, через Virtual File:

val pathTemplate = e.getData(CommonDataKeys.VIRTUAL_FILE)
val shortMainTemplate = pathTemplate.findChild(MAIN_SHORT_FILE_TEMPLATE)

Или вот так через Path:

val pathTemplate = e.getData(CommonDataKeys.VIRTUAL_FILE)?.toNioPath()
val shortMainTemplate = pathTemplate.resolve(MAIN_SHORT_FILE_TEMPLATE)

Да, вы правы.

Извиняюсь если был резок. Но я не понимаю почему вы хотите чтобы я тестировал производительность БД. Если у вас БД "задыхается" под нагрузкой или запросы написаны не оптимально, то это тема отдельного разговора. Мой тест показывает стоимость лишнего сетевого обращения к БД. И для моего теста чем быстрее ответит база тем лучше, чтобы нагляднее оценить именно эту задержку.

По поводу симметричности, да к этому можно придраться. Но как не переставляй порядок операций в моем тесте, результат будет одинаковый. Я об этом и написал в статье "что даже такой простой тест позволяет понять это". Добавил по вашей просьбе в этот же тест отдельные методы на каждый случай. На каждый запуск контейнер у меня "накатывается" с нуля (spring.sql.init.mode=always). Вообщем то я для этого и выложил все это в открытый доступ, чтобы каждый мог "поиграться" так как ему захочется. И лучше это делать как я опять же писал в статье на remote DB (например из тестового окружения), чтобы более наглядно увидеть сетевые задержки. В локальном тест контейнере они ничтожно малы. И тест с JPA показывает это - оверхед фреймворка = обращению к бд.

В статье также указано (правда без цифр) что я проводил данный тест на реальных запросах и на около-прод бд. Результат один и тот же всегда - разница в два раза на одиночных запросах(если они конечно не возвращают мегабайты данных). Цифры на память примерно указал в комментарии https://habr.com/ru/articles/803395/comments/#comment_26676749. Где у человека схожие с вашими вопросы. И как мне кажется вы не совсем понимаете стоимость лишнего сетевого вызова и к чему это ведет - лишнее время удержание потока и коннекта к бд и каким это ведет последствиям. И основной посыл моей статьи в том что не надо ставить транзакции там где они не нужны, будет только хуже. Достаточно простая истина, но приходиться повторять ее чуть ли не в каждом комментарии.

Подход с пометкой всех методов @Transactional , помимо консистентности в ряде случаев, позволяет подгружать lazy поля. И да, не все поля можно подгрузить через join fetch за один проход (см. MultipleBagFetchException). Подозреваю, это главное, почему везде лепят транзакционность. У вас в примере этот аспект не затронут, а зря.

Я указал в статье что данный кейс я не рассматриваю. Потому что JPA и его особенности это отдельная тема, а статья не про это. Я лишь указал что там также имеет место это проблема.

А потом - ну это неправильно говорить о двухкратном увеличении скорости работы методов на БД объемом в 6 строк. Если код работает 1мс и БД работает 1мс, то ускорение в 0.5мс - относительно существенное, но в абсолютных значениях оно ничтожно. Если БД отрабатывает 100мс и жава 5мс, и вы сэкономите разными приемами 10 мс, то это похвально, но при отсутствии highload'а это будет незаметно.

Судя по всему вы ничего не поняли и статью на которую я давал ссылку тоже проигнорировали. Какая разница сколько в БД строк? Я старался показать именно сетевую задержку и ее влияние. И да, в реальном мире данные от БД до приложения не доходят мгновенно, бд может и отработает как вы пишите за 1мс, только данные будут идти до приложения 100мс (а все это время мы держим коннект в базе). И более того сеть не стабильна. И по отношению к нашему основному приложению БД выступает в роли внешней системы, которая как я писал может отвалиться на коммите который не нужен, и мы получим ошибку когда данные по факту уже получили.

Величина 0.1мс это величина сетевой задержки в локальном тест контейнере. Код и бд в этом случае отрабатывают по сути мгновенно и это время можно принять за 0. Если выполнить этот тест на remote DB то результат уже будет не 0.1мс, а 10мс. А если БД находиться в “далеком” датацентре то будет уже 100мс. Итого запрос без транзакции выполняется за 100мс, а с ней за 200мс. Именно такие результаты примерно я получал когда тестировал это на рабочих стендах. Эта и есть величина паразитной сетевой задержки, которая зависит от того насколько далеко БД находиться от приложения. Вы сами можете это все проверить на своих рабочих тестовых стендах.

Посыл статьи в том что не надо ставить транзакции где попало. Будет только хуже. И величина этого "хуже" зависит от передачи данных по сети.

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

Вы точно читали статью? Стечение обстоятельств отнюдь не странное и я детально объяснил откуда это берется - из за лишнего сетевого запроса на закрытие транзакции. Можете написать на чистом JDBC запрос с транзакцией и без, тогда возможно станет яснее. 

Результаты 1-го запроса отличаются от 2-го и 3-го примерно в два раза. 2 и 3 отличаются в 1.1 раза т.е. можно сказать что они примерно равны. Особенно если запускать тест несколько раз. Т.е. вывод такой что одиночный запрос без транзакции в два раза быстрее чем с ней. Мой тест прогревает базу, если посмотреть внимательно. Я именно этого и добивался чтобы данные закешировались и показать влияние сетевой задержки.

Кроме того, объёмы данных совсем минимальные, чтобы вообще судить о производительности.

О какой производительности? у меня не было цели тестировать производительность БД. И статья не о производительности БД, а про то как мы получаем лишнюю сетевую операцию на пустом месте. Я как раз и ставил цель протестировать около идеальные условия когда данные закэшированны и нет конкуренции и БД отвечает мгновенно. В случае нагрузок БД может отвечать еще дольше и тогда разница будет еще нагляднее, но совершенно очевидно что быстрее сетевые запросы к бд выполняются точно не будут.

Кроме того, не вижу у вас индекса.

Плохо смотрите. Констрейнт уникальности автоматически создает уникальный индекс. Я все больше начинаю сомневаться в вашей компетенции. Все ваши реплики абсолютно невпопад.

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

Спасибо, но я воздержусь от вашего предложения тестировать перфоманс БД, вы судя по всему совсем не поняли посыл статьи. И совсем непонятно о каких результатах мне говорили?

Вы делаете очень странные выводы, которые основаны не понятно на чем. Я вам написал, что основной задачей которой я занимался - была оптимизация скл запросов и скорости их выполнения. Действительно спор без смысла. Какой смысл ставить транзакции везде, даже там где они не нужны, в надежде на то что далее кто то не допустит "мифическую ошибку". Как я писал в ответах на другие комментарии, практика говорит об обратном, когда в такие методы не глядя начинают добавлять вызов внешних сервисов.

Совершенно верно! Именно этот кейс я имел ввиду. Но соглашусь что моя формулировка был не верна, поэтому поправил статью.

Но справделивости ради надо заметить, что постгрес использует технологию снимков данных для работы, поэтому он по дефолту предоставляет уровень repeatable read https://habr.com/ru/companies/postgrespro/articles/442804/

Да, про это уже писали - https://habr.com/ru/articles/803395/comments/#comment_26672365

Был не корректен в своих фомрулировках, поправил статью.

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

Согласен был не прав, в столь категоричной формулировке, не смотря на то что это был не наш случай.

В два раза это микрооптимизация?

 Кто будет помнить, что на этом методе специально выключили транзакции? 

Определение транзакции достаточно простое, чтобы понять случаи где она не нужна.

А потом этот метод разрастается и мы уже там и читаем и пишем, но все уверены, что он же написан стандартно, с транзакциями и должен работать правильно

Как показывает практика именно это и приводит к реальным ошибкам. Когда на методе стоит транзакция, а в него потом пихают все что не попадя, не смотря на транзакцию.

Невыдуманные примеры из этого проекта:

1) над классом стоит транзакция с readOnly=true, пришел разраб добавил метод который меняет данные. Спринг тест это не отловил т.к. тоже был помечен транзакцией. В итоге сервис упал с ошибкой - изменение данных в читающей транзакции. По итогу было очень большое разбирательство.

2) есть метод с транзакцией без readOnly. Он потихоньку разрастается и в него добавляют потом пуш сообщения в кафку. В итогу имеем случаи когда консьюмер получит сообщение с ид сущности, которая не была еще закомичена в исходном методе. И флоу полностью сломано. И отладить такое и найти проблему было очень тяжело.

Мне кажется автор излишне драматизирует. При одном вызове я лично в своих проектах не нашел оборачивания в транзакцию

Может потомучто вы не используете там ручное управления транзакциями?)

ничего критического не вижу.

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

Information

Rating
3,775-th
Location
Рязань, Рязанская обл., Россия
Registered
Activity

Specialization

Specialist
Lead
Java
SQL
Database