Комментарии 44
Так тебе ведь сказали, что им скорость не важна, а так ошибок меньше выходит? Если я верно понял. Если ресурсов завались и не жалко, то вероятно и шут с ними, пусть себе ставят. При таком подходе наверное выходит один запрос в апи это одна транзакция в базе :)
В статье нет ни слова что скорость не важна, а даже есть цитаты что они опрадвывали это перфомансом. Да я не прописал это явно, но основная проблема которой приходилось заниматься это как раз оптимизация запросов, чтобы меньше удерживать коннекты.
Как это нет ни слова? Тебя с оптимизациями, по твоим же словам, послали. Значит скорость не важна, ты, судя по всему, этого просто так и не понял, а объяснять не стали. В итоге ты уволился.
Но спор без смысла. Итог, продолжат ставить так как ошибок меньше и это важнее.
Вы делаете очень странные выводы, которые основаны не понятно на чем. Я вам написал, что основной задачей которой я занимался - была оптимизация скл запросов и скорости их выполнения. Действительно спор без смысла. Какой смысл ставить транзакции везде, даже там где они не нужны, в надежде на то что далее кто то не допустит "мифическую ошибку". Как я писал в ответах на другие комментарии, практика говорит об обратном, когда в такие методы не глядя начинают добавлять вызов внешних сервисов.
Это распространенное непонимание. Есть то, что надумади те кто тебя посадил оптимизациями заниматься, а есть факт, что ты в итоге никому ничего доказать не смог. Вывод прост, одно дело, что люди говорят, а другое факт того, что ты уже не там. Отличать реальное функционирование процессов от словес надо уметь, экономит время и нервы.
А мы-то, в роли пользователей, этого самого "сервиса, чье приложение установлено в смартфоне большинства жителей РФ", недоумеваем, почему запуск вот этой небольшой программки занимает сто-о-олько времени на не самом хилом смартфоне.
А ведь и правда, я стал замечать, что приложения с некоторых пор полюбили при запуске показать свою заставку - чего еще несколько лет назад особо не было (и запускались заметно быстрее, хотя и связь, и сами смарты были попроще в смысле скорости).
Это потому что сначала функционала мало. Приложение выполняется быстро.
Потом разработчики начинают навешивать всякие свистелки и перделки, и, в результате, через некоторое время классное быстрое приложение превращается в мало функционального тормозного монстра. Так происходит, увы, почти со всеми приложениями: каждый следующий релиз хуже предыдущего (хотя, по идее, должно быть наоборот).
Хорошая версия, но, кажется, это еще и от рук и желания компании выпустить нормальный продукт.
А то, знаете, я вот удивляюсь: Телеграм мобильный был поначалу легковесным. Сейчас растолстел, но все равно не перегоняет вотсап, который с самого начала был пухлячком "а что такого, будут юзать любой". Но что в телеге, что в любом мессанджере сообщения приходят устойчиво.
А мобильные приложения банков со встренными чатиками поддержки - ни в одном пока идеально работающего чата я не встречал. Что, в банках - не умеют писать код? Наверное, умеют, но не хотят в смысле самой компании, так видится.
Не говоря что банки рода ВТБ просто не понимают, как должно вести себя нормальное мобильное приложение на одном из топовых процов в мире с кучей ОЗУ - их убожество всегда работает "в среднем отвратительно". Талант!
Еще хотел бы вернуться к «И многие ставят аннотацию Transactional над методом где выполняется несколько SQL запросов на чтение». По моему мнению это также бессмысленно. Во‑первых, это противоречит сути определения транзакции и плюс как показано выше добавляет лишний сетевой вызов, а во‑вторых, мы так дольше удерживаем соединение.
Транзакции для нескольких запросов на чтение необходимы для обеспечения консистентности данных, см. Transaction Isolation Levels#Serializable. Что тут противоречит сути определения транзакции я как-то затрудняюсь придумать...
Да тут я наверное погорячился. Но я вот что то сходу не могу придумать реального примера когда мне потребуется Isolation Levels#Serializable для читающих транзакций, чтобы по сути все работало в однопоток, и остальные потребители ждали завершения чтения. Как правило все необходимые данные можно получить в один запрос или в такой изоляции нет смысла исходя из задач предметной области. У нас такой потребности точно не было.
Можно и без serializable, достаточно repeatable read, в общем-то. Зависит от данных, их нормализации, логики и всего прочего. Мне лень придумывать реальный пример, но не зря ж это во все серьёзные дазы банных завезли? 8)) Навскидку, скажем, получение кол-ва ответов в теме и самих ответов (в каком-нибудь диапазоне) для какого-нибудь форума, я уж не знаю...
чтобы по сути все работало в однопоток, и остальные потребители ждали завершения чтения
Откуда тут однопоток возьмётся? ДБ всё-таки не настолько дубово устроены.
У нас такой потребности точно не было.
Для множества случаев вообще и read uncommitted сойдёт, "не деньги считаем"(с)
А вот как раз без уточнения уровня изоляции этот момент просто нельзя рассматривать. И что-то мне подсказывает, что люди, которые просто бездумно на все вызовы ставят Transactional, приблизительно всегда работают с read committed, где транзакция не приносит никакой пользы в случае только чтения
Совершенно верно! Именно этот кейс я имел ввиду. Но соглашусь что моя формулировка был не верна, поэтому поправил статью.
Но справделивости ради надо заметить, что постгрес использует технологию снимков данных для работы, поэтому он по дефолту предоставляет уровень repeatable read https://habr.com/ru/companies/postgrespro/articles/442804/
Вы дали ссылку на статью Postgres Pro.
PostgreSQL же - по умолчанию предоставляет уровень `read committed`: https://www.postgresql.org/docs/current/transaction-iso.html
13.2.1 -> Read Committed is the default isolation level in PostgreSQL.
Вы не правы. При любом уровне изоляции, если выполняется более одного запроса, последующие запросы могут считать запись зафиксированную до завершения предыдущих запросов. Так что проблемы с консистентностью данных возможны при любом уровне изоляции.
последующие запросы могут считать запись зафиксированную до завершения предыдущих запросов.
Уточните: что вы имели в виду? Если запись с изменением, зафиксированным после момента начала транзакции с уровнем изоляции снимка, но до первого обращения к записи в этой транзакции, то утверждение неверно. По крайней мере, в общем случае, в схеме хранения с поддержкой снимков: там у СУБД достаточно информации, чтобы найти старую версию записи и вернуть ее, даже если к этой записи транзакция ранее не обращалась.
И ЕМНИП оно реально неверно (было, в те давние времена) для конкретного Interbase 4. Тамошняя приблуда для интерактивной работы с БД - wisql.exe - при посылке первого же запроса открывала транзакцию (с изоляцией формально, если по тогдашним стандартам - REPEATBLE READ, но по жизни это был снимок, в Interbase была многоверсионная схема хранения), и сидела в ней, пока транзакцию явно не завершишь. В результате при отладке прогаммы на Delphi можно было внезапно не увидеть в wisql сделанное программой и зафиксированное изменение, потому что час назад в wisql был сделан и забыт запрос к совершенно другой таблице, а транзакция так и осталась висеть.
Уточните: что вы имели в виду?
Я опровергаю утверждение
люди, которые просто бездумно на все вызовы ставят Transactional, приблизительно всегда работают с read committed, где транзакция не приносит никакой пользы в случае только чтения
Иными словами, если клиент явно не использует транзакцию (вызов не Transactional), то каждый запрос, которой приходит от клиента выполняется в своей транзакции. А значит и снимок формируется для каждого запроса по отдельности.
Вы, похоже, не так поняли утверждение (не свосем однозначное, да) , однако сразу кинулись его опровергать. А я вот так понял, что утверждавший имел в виду вызов метода с несколькими запросами на чтение к БД внутри него - а в этом сценарии уровень изоляции имеет значение.
Ну, для вас такая безапелляционность - это норма. А вот я пока что, прежде чем спорить дальше, попробую подождать уточнения от @martin_wanderer
Вы, похоже, не так поняли утверждение
А как его еще понимать?
бездумно на все вызовы ставят Transactional
транзакция не приносит никакой пользы в случае только чтения
Если не поставить Transactional на вызов, содержащий несколько запросов только чтения, то вне зависимости от уровня изоляции можно прочитать неконсистентные данные.
Вы собрались это оспорить?
А как его еще понимать?
Я же написал. Читайте.
Если не поставить Transactional на вызов, содержащий несколько запросов...
А если поставить (да ещё и бездумно, как говорил автор обсуждаемого комментария)? Прямо "над методом где выполняется несколько SQL запросов на чтение " (цитата из оригинального, до правки, варианта статьи)? Такой вариант прочтения вам в голову не приходил?
Так что предлагаю не заниматься тут герменевтикой, а подождать, что скажет автор обсуждаемого комментария.
Я уже не знаю, по буквам что-ли разбирать?
бездумно на все вызовы ставят Transactional, приблизительно всегда работают с read committed, где транзакция не приносит никакой пользы в случае только чтения
Установка Transactional на вызов, содержащий только один запрос, причем не важно на чтение или модификацию, смысла не имеет. Так как любой запрос выполняется в транзакции и "только чтение" не играет никакой роли. С этим согласны?
Если в вызове больше одного запроса, даже если они только на чтение, то не установка Transactional может привести к неконсистентности данных при любом уровне изоляции. С этим согласны?
Значит выделенная мной фраза некорректна.
Я уже не знаю, по буквам что-ли разбирать?
Нет, досточно указать где написано про "вызов, содержащий только один запрос" в комментарии, на который вы решили возразить- причем, явно написано, а не между строк. Потому что если ещё и учесть контекст в виде предыдущего комментария и цитаты из статьи в нем, то очевидно (мне, по крайней мере), что речь идет об установки одной транзакции на вызов, содержащий несколько запросов.
Но вообще я ещё раз предлагаю вам не заниматься тут "лингвистической экспертизой", а дождаться ответа на прямо поставленный вопрос автору высказывания.
Если в вызове больше одного запроса, даже если они только на чтение, то не установка Transactional может привести к неконсистентности данных при любом уровне изоляции. С этим согласны?
С этим я и не спорил, я с самого начала говорил про одну тразакцию на несколько запросов, и даже пример про это привел. И что с того?
"вызов, содержащий только один запрос"
К вызову, содержащему только один запрос, эта фраза относиться не может:
транзакция не приносит никакой пользы в случае только чтения
Так как вызов из одного запроса всегда в транзакции, в не зависимости от того для чтения он или модификации и в не зависимости от того в блоке транзакции он или нет.
очевидно (мне, по крайней мере), что речь идет об установки одной транзакции на вызов, содержащий несколько запросов.
Но тогда утверждение "транзакция не приносит никакой пользы в случае только чтения" некорректно. ЧТД.
Вы точно не запутались в том, что вы хотите мне доказать? А то я вас прошу "указать где написано про вызов, содержащий только один запрос" в некоей фразе , а вы мне стремитесь доказать, что вообще " К вызову, содержащему только один запрос, эта фраза относиться не может". Ну, если так, то я даже обсуждать это ваше доказательство не буду - самого по себе признания моей правоты мне хватит.
Или вы что-то другое хотели написать?
PS То что вы хотели на самом деле сказать, вы уже уточнили, и против конкретно именно этого я, как уже написал выше, не возражаю. Я возражаю против однозначности вашей интерпретации чужой фразы. Но по этому поводу, ещё раз повторю, лучше переспросить автора фразы.
Мне кажется автор излишне драматизирует. При одном вызове я лично в своих проектах не нашел оборачивания в транзакцию. Да и в целом если в методе нет i\o или других вычислений, которые бы держали коннект с базой, ничего критического не вижу. Так что it depends как говорится.
Мне кажется автор излишне драматизирует. При одном вызове я лично в своих проектах не нашел оборачивания в транзакцию
Может потомучто вы не используете там ручное управления транзакциями?)
ничего критического не вижу.
Ну как сказать.. когда самые горячие методы апи это как раз одиночные запросы к БД, и если убрать транзакцию то скорость работы и пропускная способность этих методов возрастет вдвое.
Убирание транзакций - это микрооптимизации на пустом месте, которые в будущем могут привести к большим проблемам. Кто будет помнить, что на этом методе специально выключили транзакции? Да никто. А потом этот метод разрастается и мы уже там и читаем и пишем, но все уверены, что он же написан стандартно, с транзакциями и должен работать правильно. И здравствуй баги Гейзенберга и иже с ними...
В два раза это микрооптимизация?
Кто будет помнить, что на этом методе специально выключили транзакции?
Определение транзакции достаточно простое, чтобы понять случаи где она не нужна.
А потом этот метод разрастается и мы уже там и читаем и пишем, но все уверены, что он же написан стандартно, с транзакциями и должен работать правильно
Как показывает практика именно это и приводит к реальным ошибкам. Когда на методе стоит транзакция, а в него потом пихают все что не попадя, не смотря на транзакцию.
Невыдуманные примеры из этого проекта:
1) над классом стоит транзакция с readOnly=true, пришел разраб добавил метод который меняет данные. Спринг тест это не отловил т.к. тоже был помечен транзакцией. В итоге сервис упал с ошибкой - изменение данных в читающей транзакции. По итогу было очень большое разбирательство.
2) есть метод с транзакцией без readOnly. Он потихоньку разрастается и в него добавляют потом пуш сообщения в кафку. В итогу имеем случаи когда консьюмер получит сообщение с ид сущности, которая не была еще закомичена в исходном методе. И флоу полностью сломано. И отладить такое и найти проблему было очень тяжело.
Это не проблема транзакций, а кривизна рук разработчиков.
Есть золотое правило - не отправлять в топики сообщения, когда у тебя активна транзакция. Все.
Если во время отправки упало - повтори все по новой, читай про идемпотентность
Всё зависит от задач. Перфоманс важен, но единообразие стиля кода, когда отдельный разработчик не нагружает себя думами, надо ли тут транзацияюставить или нет. Видимо, уверенность, что не упадет, была важнее, чем прирост в скорости выполнения запросов к базе. А вопрос, правы ли Вы были, он двусмысленный ) Если это про то, что правильно ли уволился, то теперь уже поздно рассуждать) Очевидно, что характерами с командой точно не сошлись.
транзакция нужна когда у нас есть несколько операций, которые меняют данные в БД.
Это верно только если в транзакции лишь один запрос. Даже при READ COMMITED второй запрос может считать запись, зафиксированную до завершения первого запроса. А это потенциальные проблемы с консистентностью данных.
Да, про это уже писали - https://habr.com/ru/articles/803395/comments/#comment_26672365
Был не корректен в своих фомрулировках, поправил статью.
Возможно я что-то упускаю, но есть подозрение, что тест, который Вы опубликовали, не вполне представителен.
Как я понимаю, Вы выполняете одни и те же запросы на одной базе данных. И по "странному" стечению обстоятельств первый тест медленнее второго, а второй медленнее третьего. Это может говорить о том, что база данных "прогрелась" и одни и те же запросы достаются из кэша.
Кроме того, объёмы данных совсем минимальные, чтобы вообще судить о производительности.
Кроме того, не вижу у вас индекса.
Кроме того, не вижу параллельного выполнения запросов.
Рекомендую для большей наглядности тестов, добавить в базу 10 миллионов записей, выполнять тесты в 100 потоков и каждый тест выполнять на новом инстансе базы данных.
Тогда возможно получите результаты более близкие к тем, о которых Вам говорили.
Как я понимаю, Вы выполняете одни и те же запросы на одной базе данных. И по "странному" стечению обстоятельств первый тест медленнее второго, а второй медленнее третьего. Это может говорить о том, что база данных "прогрелась" и одни и те же запросы достаются из кэша.
Вы точно читали статью? Стечение обстоятельств отнюдь не странное и я детально объяснил откуда это берется - из за лишнего сетевого запроса на закрытие транзакции. Можете написать на чистом JDBC запрос с транзакцией и без, тогда возможно станет яснее.
Результаты 1-го запроса отличаются от 2-го и 3-го примерно в два раза. 2 и 3 отличаются в 1.1 раза т.е. можно сказать что они примерно равны. Особенно если запускать тест несколько раз. Т.е. вывод такой что одиночный запрос без транзакции в два раза быстрее чем с ней. Мой тест прогревает базу, если посмотреть внимательно. Я именно этого и добивался чтобы данные закешировались и показать влияние сетевой задержки.
Кроме того, объёмы данных совсем минимальные, чтобы вообще судить о производительности.
О какой производительности? у меня не было цели тестировать производительность БД. И статья не о производительности БД, а про то как мы получаем лишнюю сетевую операцию на пустом месте. Я как раз и ставил цель протестировать около идеальные условия когда данные закэшированны и нет конкуренции и БД отвечает мгновенно. В случае нагрузок БД может отвечать еще дольше и тогда разница будет еще нагляднее, но совершенно очевидно что быстрее сетевые запросы к бд выполняются точно не будут.
Кроме того, не вижу у вас индекса.
Плохо смотрите. Констрейнт уникальности автоматически создает уникальный индекс. Я все больше начинаю сомневаться в вашей компетенции. Все ваши реплики абсолютно невпопад.
Рекомендую для большей наглядности тестов, добавить в базу 10 миллионов записей, выполнять тесты в 100 потоков и каждый тест выполнять на новом инстансе базы данных. Тогда возможно получите результаты более близкие к тем, о которых Вам говорили.
Спасибо, но я воздержусь от вашего предложения тестировать перфоманс БД, вы судя по всему совсем не поняли посыл статьи. И совсем непонятно о каких результатах мне говорили?
Про индекс согласен, не заметил уникальный ключ. В остальном, Вы тестируете производительность трёх разных способов выполнения этого запроса. Это, разумеется, не то же самое, что производительность базы данных.
Но условия для выполнения тестов, во-первых, не приближены к реальным, а, во-вторых, не симметричные. Я всего лишь вежливо предложил этот тест адаптировать и посмотреть, что получится. Не понимаю, откуда столько агрессии.
Готов дискутировать по деталям, но только при условии уважительной дискуссии.
Извиняюсь если был резок. Но я не понимаю почему вы хотите чтобы я тестировал производительность БД. Если у вас БД "задыхается" под нагрузкой или запросы написаны не оптимально, то это тема отдельного разговора. Мой тест показывает стоимость лишнего сетевого обращения к БД. И для моего теста чем быстрее ответит база тем лучше, чтобы нагляднее оценить именно эту задержку.
По поводу симметричности, да к этому можно придраться. Но как не переставляй порядок операций в моем тесте, результат будет одинаковый. Я об этом и написал в статье "что даже такой простой тест позволяет понять это". Добавил по вашей просьбе в этот же тест отдельные методы на каждый случай. На каждый запуск контейнер у меня "накатывается" с нуля (spring.sql.init.mode=always). Вообщем то я для этого и выложил все это в открытый доступ, чтобы каждый мог "поиграться" так как ему захочется. И лучше это делать как я опять же писал в статье на remote DB (например из тестового окружения), чтобы более наглядно увидеть сетевые задержки. В локальном тест контейнере они ничтожно малы. И тест с JPA показывает это - оверхед фреймворка = обращению к бд.
В статье также указано (правда без цифр) что я проводил данный тест на реальных запросах и на около-прод бд. Результат один и тот же всегда - разница в два раза на одиночных запросах(если они конечно не возвращают мегабайты данных). Цифры на память примерно указал в комментарии https://habr.com/ru/articles/803395/comments/#comment_26676749. Где у человека схожие с вашими вопросы. И как мне кажется вы не совсем понимаете стоимость лишнего сетевого вызова и к чему это ведет - лишнее время удержание потока и коннекта к бд и каким это ведет последствиям. И основной посыл моей статьи в том что не надо ставить транзакции там где они не нужны, будет только хуже. Достаточно простая истина, но приходиться повторять ее чуть ли не в каждом комментарии.
Подход с пометкой всех методов @Transactional
, помимо консистентности в ряде случаев, позволяет подгружать lazy поля. И да, не все поля можно подгрузить через join fetch за один проход (см. MultipleBagFetchException). Подозреваю, это главное, почему везде лепят транзакционность. У вас в примере этот аспект не затронут, а зря.
А потом - ну это неправильно говорить о двухкратном увеличении скорости работы методов на БД объемом в 6 строк. Если код работает 1мс и БД работает 1мс, то ускорение в 0.5мс - относительно существенное, но в абсолютных значениях оно ничтожно. Если БД отрабатывает 100мс и жава 5мс, и вы сэкономите разными приемами 10 мс, то это похвально, но при отсутствии highload'а это будет незаметно.
Подход с пометкой всех методов
@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мс. Именно такие результаты примерно я получал когда тестировал это на рабочих стендах. Эта и есть величина паразитной сетевой задержки, которая зависит от того насколько далеко БД находиться от приложения. Вы сами можете это все проверить на своих рабочих тестовых стендах.
Посыл статьи в том что не надо ставить транзакции где попало. Будет только хуже. И величина этого "хуже" зависит от передачи данных по сети.
В spring-data-jpa проекте есть такой код:
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
И есть на него закрытая бага не открывать по умолчанию транзакцию на чтение.
Возможно JPA репозиторий должен работать с разного рода вендорами баз данных и неизвестно, как они реализуют транзакционность одиночных команд типа select. Для Postgres и, судя по Discuss about the optimization, для MySQL это может быть оверхед. Ссылка в баге на Hibernate устарела, вот верная
Те мы свои кастомные getByIsbn методы чтения можем делать без транзакций, а стандартные data-jpa все равно с флагом readOnly пойдут. Можете переоткрыть issue со своими выкладками - будет аргумент для архитекторов, если протолкнуть. Но там вряд ли согласятся...
Опять транзакции…