Telegram Web
Вышла Java 23🎉

Это не LTS версия, почти все фичи в превью стадии. Этап важный, но не самый интересный.

Поэтому подниму другую тему.

Я уже несколько лет делюсь знаниями в этом канале. У меня хорошо получается видеть главное и подсвечивать неочевидные моменты. Хочу продолжать в том же духе.

Помогите мне с контент-планом:)

В чем вам помочь разобраться? Какие знания хочется углубить?

Заполните, пожалуйста, эту форму. Всего 3 вопроса!
Please open Telegram to view this post
VIEW IN TELEGRAM
GraalVM, часть 1

Последние годы на конференциях встречаются доклады про GraalVM. У них мало просмотров, тк есть мнение, что Грааль — что-то экспериментальное и далёкое от коммерческой разработки.

Но нет!

GraalVM — проект, который объединяет в себе целую смесь идей и технологий. В этом посте расскажу о самом близком к практике и понятном компоненте — JIT-компиляторе Graal и зачем он нужен.

Начнем с основ:
✍️ Программист пишет java код
✍️ Компилятор превращает его в байт-код
✍️ JIT-компилятор внутри JVM переводит байт-код в системные вызовы для конкретной ОС

JVM использует два типа компиляторов: С1 и С2:
🐍 С1 (client) — для десктопа, браузера и приложений на слабом железе. Быстрый старт, низкое потребления памяти, но мало оптимизаций
🐊 С2 (server) — для больших и мощных серверов. Алгоритмы сложнее, памяти нужно больше, зато на выходе более оптимизированный нативный код

Разделение было актуально 20 лет назад. Сейчас железо мощнее и дешевле, поэтому с java 8 работает смешанный тип (tiered compilation) с участием обоих компиляторов.

Технологии позволяют сделать компиляцию быстрее, но вносить серьезные изменения в С1 и С2 проблематично, так как код сложный и запутанный.

Тот случай, когда проще написать заново:)

В Java 9 работа с JIT-компилятором скрылась за отдельным интерфейсом JVMCI — JVM Compiler Interface. Теперь можно собрать JVM с другим компилятором. Например, с новым и свежим JIT компилятором Graal.

В OpenJDK включается флажками
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler

Работает пока только для Linux, иногда показывает небольшой прирост скорости. Так что имеет смысл попробовать!

В GraalVM есть ещё одно интересное направление, и о нем расскажу в следующем посте🔥
GraalVM, часть 2: native image

Продолжаем вникать в GraalVM. Сегодня поговорим о самой интересной его части — native image билдер.

Он компилирует java код и все нужные классы сразу в бинарный файл. Нет промежуточного этапа в виде байткода, а значит для запуска не нужна JVM.

Джава код запускается без JVM🤯

Бинарник собирается под конкретную платформу, поэтому код больше не write once, run anywhere! Одна из киллер фич java отправляется на антресоль.

Что получаем от такого богохульства:
Быстрый старт, тк все классы уже загружены
😐 Немного усложняется CI и тестирование
😡 Нужно адаптировать код

Например, статические блоки по классике выполняются при загрузке класса. Если класс не загружается, блок не выполнится.

Это легко чинится переносом кода из статики в другое место. Таких нюансов довольно много, но почти для всех есть обходные пути

😡 Если приложение использует библиотеки, они тоже должны предоставлять нейтив вариант
😡 Долгая компиляция

Сама с native images не сталкивалась, но видела доклад, как их используют в одном из проектов Сбера. Так что вещь рабочая, хоть и специфичная.

Spring native image🍃

Интересный проект по адаптации Spring под нейтив имедж. Обычные спринг приложения стартуют несколько минут. Ожидается, что native вариант запустится за несколько секунд!

Сложность в том, что в спринге большая часть магии происходит на старте. Активно используется рефлекшн и создаются прокси-классы. Чтобы адаптировать Spring под native, надо переписать огромную часть кода.

Плюс не все библиотеки легко адаптируются под нэйтив. Например, сейчас нет поддержки Mockito. А куда мы без него?

Поэтому сохраняется интрига — непонятно, что в итоге получится, и стоит ли оно того. Но следить за этим очень интересно🥰

Подведем итог

GraalVM — проект, объединяющий несколько направлений:
🌸 JIT компилятор. Уже сейчас можно запустить на OpenJDK и OracleJDK и чуточку ускорить приложение
🌸 Виртуальная машина GraalVM
🌸 Сборщик native image. Интересен и сам подход, и проекты, которые его используют, например, Spring native image

Есть в GraalVM и другие штуки, например, мультиязычный рантайм. Но они вряд ли пригодятся большинству из нас, поэтому не будем о них😊
Какая стратегия деплоя требует больше всего ресурсов в инфраструктуре?
Anonymous Poll
16%
Rolling update
36%
Blue-Green
14%
Canary
18%
A/B testing
16%
Shadow deployment
Стратегии деплоя

– забота не только девопсов, но и разработчиков уровня сеньор+. В этом посте обозначу, какие схемы бывают, и в чем их суть.

Все сценарии работают на одну из двух ситуаций:

Ситуация 1: нужно обновить сервисы на проде с версии 1 на версию 2

🌔 Rolling update

Добавляем инстанс с версией 2 и переводим туда часть запросов. Если все ок, понемногу добавляем сервисы с версией 2 и убираем с версией 1

👯 Blue-green

У нас всегда в наличии 2 идентичных продакшн среды. На одной функционирует версия 1 и обрабатывает все запросы, другая простаивает.
▫️ Раскатываем сервисы версии 2 на неактивный стенд
▫️ Переключаем роутер с одного стенда на другой

На практике чаще встречается Rolling update, потому что это дешевле.

Ситуация 2: хотим протестировать фичу в продакшене😇

🎏 AB testing

▫️ Запускаем несколько экземпляров сервиса с новой фичей и направляем туда часть запросов
▫️ Сравниваем метрики старой и новой версии
▫️ Выбираем победителя
▫️ Раскатываем его на всех пользователей

Способ часто используется для изменений на фронте.

🐤 Канареечный деплой (canary)

Поднимаем сервис с новой версией, переводим туда трафик от определенной группы пользователей. Если все ок, раскатываем новую версию дальше.

Технически похоже на rolling update, но в случае канарейки мы явно указываем, каким пользователям видна новая версия. Это могут быть бета тестеры или представители заказчика.

Используется такой способ редко, лично ни разу не встречала.

🌚 Shadow deployment

▫️ Поднимаем сервис с новой версией
▫️ Дублируем туда трафик с продакшена
▫️ Смотрим, как сервис справляется (или нет🥲)

Цель shadow deployment - проверить фичи на бэкенде под реальной нагрузкой. Иногда поднимается полная копия продакшен среды. Но чаще все же ограниченный набор сервисов.

Как реализовать эти стратегии?

Основную работу делают девопсы с помощью bash скриптов, Kubernetes, Consul, Istio и их аналогов.

Со стороны разработки может понадобиться
▫️ добавить механизмы включения/выключения фич (feature toggle)
▫️ обеспечить совместимость старой и новой версии по данным и API. На практике бывает сложно, по миграциям данных даже целые книжки пишут!

Ответ на вопрос перед постом – больше всего ресурсов требует Blue-green стратегия. Также принимается вариант с shadow deployment на максималках🔥
Какой хэш отличается от остальных?
Какой хэш отличается от остальных в коде выше?
Anonymous Poll
9%
hash1
5%
hash2
50%
hash3
15%
Все хэши разные
21%
Все хэши равны
Критикую JDK: метод hashCode

Сегодня на простом примере из JDK покажу, как НЕ нужно проектировать апи. Обычно всю теорию по этой теме можно свести к “делайте хорошо, а плохо не делайте”. Разбор ошибок - очень наглядный способ посмотреть все на практике.

Поэтому углубимся в два метода класса Objects:
Objects.hashCode(Object)
Objects.hash(Object…)


Человек, который видит их в первый раз, обязательно спросит
🤔 Чем отличается hash и hashCode?
🤔 Почему они выдают разные результаты?


(ответ на вопрос перед постом: hash3 отличается от остальных хэшей)

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

В нашем случае приходится лезть в доку и исходный код👎

Что увидим внутри:
▫️ hashCode принимает 1 аргумент - Object, и возвращает его хэш
▫️ hash принимает массив объектов переменной длины, итоговый хэш считается на основе переданных полей. Чтобы учесть порядок, hash производит дополнительные вычисления, даже для одного поля. Поэтому итоговые хэши отличаются.

Супер, я разобралась, но зачем мне в это погружаться? Я хочу просто посчитать хэш объекта, а не вникать в разницу 2 методов. Тратить даже 5 минут на такую простейшую задачу отвратительно. Bad user experience во всей красе.

В случае хэшей исправить ситуацию элементарно, просто дать методам одинаковые имена:
Objects.hashCode(Object)
Objects.hashCode(Object…)


Если передать один обьект, java не запутается и вызовет нужный метод. Плюсы очевидны:
Не надо думать, какой метод вызвать
Хэшкод обьекта будет одинаков в любом месте кода

Почему так не сделано изначально?

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

Видимо автор решил, что и так сойдёт.

Оставим это на совести разработчиков JDK, а себе заберём следующие выводы:
▫️ В хорошем апи пользователю не нужно смотреть документацию, чтобы сделать базовые вещи
▫️ Если в апи есть неочевидные моменты, их надо поправить, а не писать в доке warning
▫️ Если пользователь пошел смотреть исходники, чтобы разобраться - это полный провал

Чем сложнее система, тем важнее вкладываться в простоту. Простоту понимания, чтения, поддержки. Даже собственный код через пару месяцев выглядит как чужой. Понятные названия переменных, классов и методов, непротиворечивость - база для комфортной работы❤️

Это уже не первый пост, где я разбираю неудачные методы в JDK. Если вам хочется больше, предлагаю почитать
🤦 Критикую метод HashMap из java 20
🤦 Критикую методы StreamAPI в java 22
🤦 Критикую методы BigDecimal
Spring Security: основная архитектура

Разгребала на выходных опрос по непоняткам в спринге. Один из самых частых вопросов касается Spring Security.

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

К счастью, с годами ситуация прояснилась. Секрет успеха по работе с Security лежит в понимании двух вещей:

🔶 Используемый механизм безопасности

LDAP, JWT, разные схемы oAuth и SSO. Роли, белые/черные списки, секреты, токены и тд. В чистом виде большинство механизмов несложные, на ютубе отлично объясняются за 10 минут.

🔶 Базовая архитектура Spring Security

Об этом и будет пост. Опишу простыми словами, что происходит в классической (не реактивной) архитектуре, и как решать задачки с секьюрити на практике.

Spring Security участвует в двух процессах:

1️⃣ Путь запроса до контроллера и обратно

Схема такая:
▪️ Сервер получает запрос
▪️ Проводит его через цепочку фильтров, у каждого из которых своя задача:
    ▫️ CsrfFilter проверяет CSRF токен
    ▫️ SessionManagementFilter разбирается с сессией
И так далее, обычно 10-15 фильтров в итоге
▪️ Запрос попадает в контроллер
▪️ Выполняется бизнес-логика проекта
▪️ Контроллер возвращает ответ
▪️ Ответ проходит по той же цепочке фильтров, но в обратном порядке. Фильтры выставят у ответа нужные заголовки, обнулят/сохранят сессию и тд
▪️ Ответ возвращается пользователю

Что тут важно:
▫️ Проверка прав, заголовков и многие секьюрити штуки часто обрабатываются в фильтрах ДО вызова контроллера
▫️ Можно поменять содержимое, заголовки и другие части ответа ПОСЛЕ выхода из контроллера
▫️ Каждый фильтр может менять запрос/ответ или контекст, поэтому порядок фильтров важен
▫️За конфигурацию фильтров отвечает метод конфига
WebSecurityConfigurerAdapter#configure(HttpSecurity http)

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

2️⃣ Авторизация/аутентификация
скрывается за фасадом — бином AuthenticationManager.

Чтобы авторизовать пользователя, в коде проекта нужно вызвать метод
authenticationManager.authenticate

Может в фильтре, может в контроллере, зависит от механизма безопасности.

Метод authenticate пройдётся по всем заданным источникам авторизации. Результат можно положить в SecurityContextHolder, тогда он будет доступен из любого места в коде. Также бросится событие AuthenticationSuccessEvent, но обычно его игнорируют:)

Источники авторизации называются *Provider (например, DaoAuthenticationProvider) и определяются в конфиге. В итоге всё, что нужно от разработчика это:

☝️определить провайдеры
✌️ вызвать authenticate в  нужном месте

Посмотреть список текущих провайдеров: брейкпойнт в методе  ProviderManager#authenticate.

Задать список провайдеров: метод configure(AuthenticationManagerBuilder auth) в классе WebSecurityConfigurerAdapter

🦄Что с этим знанием делать на практике

Если вам дали задачу, связанную с Spring Security, не бросайтесь сразу гуглить

spring security jwt/oauth/ldap example

Шанс запутаться и сделать не то стремится к 100%. Если у вас крупный и сложный проект, идеально подходящего туториала точно не будет.

Лучше проработать каждую часть по порядку:

🛡️ Разобраться в механизме безопасности, который нужно реализовать. Что, откуда и как передаётся, где, когда и как валидируется и сохраняется.

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

🛡️ Прикинуть, как полученная схема ложится на архитектуру секьюрити. Что сделать в фильтрах до обработки запроса, а что после. Возможно, что-то надо сделать внутри слоя сервисов

🛡️ Желательно обсудить полученное решение со старшим товарищем

Только после этого смотрите документацию, туториалы и собирайте из них решение.

Действуя по плану выше, вы потратите меньше времени и сохраните высокую самооценку🙂
Зачем нужен и чем плох Optional.stream()?

Когда ты в разработке 10+ лет, сложно чему-то удивиться. Кажется, что JDK изучен вдоль и поперек, и этот мир абсолютно понятен.

Но недавно мне встретился странный зверёк метод, и это вылилось в очередной пост серии “критикую JDK”.

Допустим, вы увидели в коде Optional.stream(). Попробуем угадать, что он делает.

Может быть, метод нужен, чтобы сделать из Optional стрим и использовать методы Stream API?

Хотя интернет предлагает нам статьи с подобными примерами, включим здравый смысл.

Зачем так усложнять код? Зачем из одного значения делать стрим?
Оставим авторов статей молча краснеть и продолжим исследование.

📚 В документации находим больше интересных деталей:

▫️ Если в Optional есть значение, метод вернёт стрим с этим значением.
▫️ Если Optional пустой, вернётся пустой стрим.

Также узнаем, что метод предназначен для работы с flatMap.

Например, некоторые пользователи указывают емейл, а некоторые нет. Если getEmail возвращает Optional, список емейлов получаем так:
List emails = users.stream().map(User::getEmail).flatMap(Optional::stream).toList();

А теперь критика. Ваша любимая часть:) Что с этим методом не так, и как можно сделать лучше?

Метод нужен, чтобы вытащить данные из набора Optional. В одну строчку это не сделать, минимум в две:
.filter(Optional::isPresent)
.map(Optional::get)

Новый метод делает все в одну строку, но

Почему. Такой. Ужасный. Дизайн.

👎 Метод находится в классе Optional, но бесполезен для Optional обьекта
👎 Метод заточен только под flatMap
👎 Выглядит непонятно. flatMap(Optional::stream) похож на набор слов, приходится напрячься, чтобы его понять. Сравните с очаровательными filter, map, count, findFirst🥰
👎 Сомнительная реализация. Сначала плодим маленькие стримы из каждого элемента, потом их соединяем. Столько лишних действий!

Как можно по-другому?

Я бы добавила в Stream API отдельный метод, “очищающий” текущий стрим от лишнего. Пусть он будет универсальным:

▫️ Для стрима из Optional возвращает стрим значений, empty пропускаются
▫️ Для прочих стримов убирает null элементы

Название под вопросом. Мне нравится getValues(), но можно и получше придумать.

И всё, незачем усложнять класс Optional и плодить сложные конструкции!

Простота и ясность - наше всё. Не знаю, чем думал автор метода и куда смотрели ревьюеры. Мы с вами точно бы такое не пропустили🔥
Любимые подписчики!

Конец года - прекрасное время для благодарностей и подведения итогов. От всей души, спасибо, что читаете❤️ Спасибо за ваш интерес к разработке и повышению знаний. На таких людях наша сфера держится и развивается!

Лучшие посты этого года:
Паттерн Сага
Как быстро отдавать данные по частям
Полезное в PostgreSQL
Новая фича Java 23: Derived Record
Как считается хэшкод по умолчанию?
Нужно ли высшее образование для разработчика?
Как вредит ChatGPT
От JDBC до Spring Data

Почитайте, если что то пропустили. Или не читайте:)

Желаю вам хорошо отдохнуть на каникулах и выспаться, а в следующем году покорить новые профессиональные и финансовые высоты!

С наступающим новым годом, друзья🎄
Как избежать выгорания, часть 1

В конце года люди делятся на две группы. Одна бодро подводит итоги года и строит планы на следующий. Другая устало лежит на диване и смотрит в стену.

Сегодня хочу поговорить про выгорание.

Среди разработчиков выгорание случается сплошь и рядом. А многие живут в таком состоянии несколько лет!

Обычно под выгоранием понимают апатию и потерю интереса к работе. Но это только одна из граней. Выгорание делится на 4 стадии, каждая отличается своим “настроением”, симптомами и лечением. В этом посте обсудим первую стадию:

Накопление усталости

Обычно начинается, когда работа занимает слишком много места в жизни:
🧑‍💻 Нет четкого разделения работа/отдых, работа размазывается на весь день
🧑‍💻 В выходные часто думаете над рабочими задачами
🧑‍💻 Постоянно проходите курсы, смотрите конференции и учитесь
🧑‍💻 Нацелились на 5+ на перформанс ревью и очень стараетесь
🧑‍💻 Пришли на новый проект и хотите быстро показать отличные результаты

Ничего удивительного, если много работать, то устанешь:) Но есть и менее очевидные ситуации.

Представьте, работа вам очень нравится. Задачи интересные, коллеги вдохновляют. С работы не хочется уходить. Казалось бы, мечта.

Но в перспективе все не так радужно. И переработки, и горящие глаза со временем приводят к накоплению стресса и усталости.

Как понять, что я на первой стадии выгорания?

Вы можете чувствовать себя нормально. Большинство людей не чувствуют усталости, пока она не станет 10/10.

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

Все в целом нормально, но довольно серенько.

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

Если это звучит слишком абстрактно, вот пара приемов, как косвенно оценить свое состояние:

🤔 Посмотреть, как с активностями после работы и с остальными сферами жизни. Если на них все чаще не остаётся ни времени, ни сил, если по вечерам и на выходных хочется полежать, это плохой знак

🤔 Оценить работоспособность в привычные “пики продуктивности”. У кого-то работа идёт бодрее с утра, у других - после обеда. Кто-то в понедельник делает большую часть дел, кто-то окрыляется в четверг. Но если запланированной продуктивности не случается, значит в организме копится усталость

Что делать?

Из первой стадии выбраться довольно просто:
Временно снизить рабочие активности
Четко определить рабочее время
Спланировать вечера и выходные заранее: спорт, прогулки, общение, хобби. Что угодно, не связанное с работой

Если в первые рабочие дни после нового года вы чувствовали себя бодро, запомните это состояние. Это состояние отдохнувшего человека, оно нам нужно почаще:)

В следующем посте обсудим более тяжёлые стадии выгорания. Но давайте не попадать даже в первую. Пусть горят только огоньки под постами, а вас выгорание обойдет стороной🔥
Что делать при выгорании?

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

Вторая стадия: усталость

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

Здесь человек сам чувствует, что сильно устал. Решение очевидно - пора в отпуск! Но сознание загоняет в ловушку: чем ниже продуктивность, тем выше чувство вины, и тем сложнее разрешить себе уехать на отдых.

Но это нужно сделать. Само такое состояние не пройдет. Чем быстрее примете меры, тем быстрее вернётесь к нормальной жизни.

Как отдохнуть?

Отличная схема описана в old but gold статье Разъ*б, безделье, чилл. Хороший отдых состоит из 3 частей:
🤪 Перезагрузка. Активный отдых, путешествия, новые опыт и знакомства. Чем непривычнее, тем лучше получится отвлечься
🤗 Гедонизм. Заняться любимыми делами. Хобби, еда, сериалы, игры, что угодно. Никакого чувства вины, все для себя любимого!
🥱 Скука. Дать мозгу отдохнуть от потоков информации и впечатлений: много спать, гулять на природе, смотреть в окно, наблюдать за волнами.

Не всегда для полноценного отдыха хватает двух недель отпуска. Я обычно увольняюсь и отдыхаю 2-3 месяца. Если отдых прошел успешно, вы почувствуете, что соскучились по работе. Очень приятное чувство🥰

Но если не отдыхать и продолжать копить усталость, рано или поздно наступит

Третья стадия: обратимое истощение

Апатия трансформируется в злость. Человека все раздражает, токсичность зашкаливает, идет огромное сопротивление к работе и саботаж. Может появиться депрессия и тревожность, организм начинает разваливаться то тут, то там.

Здесь отпуск уже не поможет. Нужен длительный перерыв от полугода.

Четвертая стадия: необратимое выгорание

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

3 и 4 стадии встречаются редко, поэтому описала их кратко. В айти в основном все сидят в первой-второй.

Физическое и психическое здоровье - это база. Без него не получится достигнуть целей, реализовать свои амбиции и таланты. Так что заботимся о себе, отдыхаем почаще. Тогда найдутся силы на рабочие подвиги и профессиональный рост в новом году💪
Паттерн Bulkhead

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

В чем суть?

Название Bulkhead пришло из кораблестроения. Корабль делится на независимые отсеки. Если в одном отсеке появится дыра, водой заполнится только он. Остальные части будут в порядке, и корабль хоть как-то продолжит плыть.

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

Реализовать паттерн можно на трёх уровнях:
▫️ Docker контейнер
▫️ Приложение (jar-файл)
▫️ Внутри сервиса

На каждом уровне свои ресурсы и возможности. Пойдем по порядку:

Docker контейнер

и остальные инструменты виртуализации дают контроль над главными ресурсами - памятью и потреблением CPU:
docker run -dit --cpus="2" --memory="512m" imagename

Если в одном сервисе произойдет беда, остальные будут работать как ни в чем ни бывало.

Приложение

Здесь возможности поменьше. JVM не контролирует загруженность процессора, этим занимается ОС. Зато тщательно считает свои обьекты и занятую память, мы можем задать рамки флажками типа -Xmx.

Внутри сервиса

В java коде нельзя явно ограничить количество памяти и нагрузку на цпу. Но в нашей власти повлиять на количество потоков, как следствие - ограничить число запросов для разных эндпойнтов.
Для этого Spring предлагает аннотацию @Bulkhead:
@GetMapping(value = "/post/{id}")
@Bulkhead(name = "getPost", fallbackMethod = "postFallback")
public Post getPost(@PathVariable int id) {…}

В проперти maxThreadPoolSize настраиваем максимальное число потоков.

Ограничение запросов иногда полезно, но это ненадежная реализация паттерна Bulkhead. Нет нужного уровня изоляции, у приложения все ещё общая память. Тяжёлый SQL запрос из одного потока может выгрузить половину БД и запросто положит сервис с OutOfMemoryError.

И ещё, обратите внимание на важный момент!

Допустим, мы почитали паттерны отказоустойчивости микросервисов, нашли среди них Bulkhead и решили, что нам надо. Гуглим bulkhead example, и вся поисковая выдача будет забита спринговыми аннотациями.

Как мы уже обсудили, это не самый надёжный способ. Самое верное - ограничить память и CPU в настройках Docker/виртуалки/Kubernetes/etc. Беда в том, что слово Bulkhead в этих инструментах не используется, там limits и constraints. Поэтому такое решение может легко пройти мимо.

При работе с паттернами нужно четко понимать, какая проблема решается и за счёт чего. Не хватать первое же решение с нужным словом, смотреть не только на форму, но и на содержание🤌
Какое хэширование используется в HashMap в терминах computer science?
Anonymous Poll
49%
Открытое
15%
Закрытое
7%
Кукушечное
29%
Связанное (coalesced)
HashMap: альтернативная реализация

Если хэши, бакеты, контракт equals и hashcode для вас давно пройденный этап, то вот вопрос со звездочкой:

🤔 Можно ли сделать что-то быстрее, чем HashMap? Хотя бы для частных случаев? И как это сделать в java?

Об этом и будет сегодняшний пост. Несложный компутер саенс для расширения кругозора.

HashMap - реализация структуры данных хэш-таблица. Принцип прост: вычисляем хэш ключа, находим нужный бакет, кладём пару ключ-значение.

Нюансы начинаются, если в бакете уже что-то есть. Тогда есть два основных пути:

1️⃣ Метод цепочек (separate chaining hash tables)

Его использует HashMap. Допускаем, что в одном бакете могут быть несколько элементов, организуем их в список или дерево.

Альтернативное название - open hashing. “Открытость” означает, что данные лежат не в самом массиве, а где-то в куче.

2️⃣ Метод открытой адресации (open addressing hash tables)

Если бакет занят, вычисляем следующий, пока не найдем свободный. Самое простое - проверить соседний бакет, но есть и другие стратегии.

Как только нашли свободный, кладём туда пару ключ-значение.

Поиск в такой структуре прост:
▫️ Считаем хэш ключа
▫️ Вычисляем бакет
▫️ Смотрим, какой ключ там лежит. Если нужный - возвращаем значение
▫️ Если ключ не совпал, вычисляем новый бакет и проверяем там. Повторяем, пока не дойдем до нужного ключа или пустой ячейки

В каждом бакете максимум одно значение, которое записывается в сам бакет. Все лежит в памяти рядышком, вставка и поиск работают космически быстро🚀

Из-за того, что данные лежат в самом массиве, полученную структуру так же называют close hashing.

🤔 Получается, текущий HashMap - не самый быстрый вариант?

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

Но благодаря ссылкам можно работать с объектами произвольных размеров, легко заменять и удалять элементы. Метод цепочек - более простой и универсальный вариант, поэтому именно он используется в готовых хэш-таблицах в java, go и c++.

Метод открытой адресации побеждает лишь в определенных кейсах и сложнее в реализации, поэтому не входит в стандартные библиотеки.

🤔 Можно ли реализовать хэш-таблицу с методом открытой адресации в java?

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

Но в будущем ситуация изменится! В java вовсю идёт разработка value types — объектов с полями и методами, работа с которыми идёт как с примитивом.

Это позволит хранить данные рядом, пользоваться локальностью, чаще задействовать кэши процессора. Даст зелёный свет многим алгоритмам и структурам данных, в том числе хэш-таблице с открытой адресацией.

Ответ на вопрос перед постом: HashMap использует открытое хэширование. Ставь ❤️, если выбирал ответ сердцем, и 👍 если выбирал умом
Как найти и починить in-memory пагинацию в Spring Data JPA

N+1 - самая известная проблема в Spring Data JPA/Hibernate, но не единственная. В этом посте расскажу об in-memory пагинации.

В чем суть.

Пагинацию логично делать на уровне БД с помощью limit и offset. Но иногда Hibernate делает пагинацию на стороне клиента. Выгружает все записи из память, выбирает из них заданное количество и возвращает.

Чем плохо:
Запрос выполняется долго, тк по сети передается куча данных
Забивается оперативная память. В худшем случае получаем OutOfMemoryError

Проблема возникает при сочетании предварительной загрузки связных сущностей и Pageable.

Простой пример. У постов есть комментарии со связью OneToMany:
@Entity
public class Post {
   @OneToMany
   private List<Comment> comments;
}

Мы хотим получить 5 постов с комментариями. Чтобы избежать N+1, загружаем комментарии через EntityGraph:
@EntityGraph
List<Post> findAllPosts(PageRequest.of(0, 5));

Метод findAllPosts вернёт 5 постов, как мы и просили. Но в логах увидим SQL запрос без лимитов, в память загрузятся все посты.

Как найти места со скрытой пагинацией?

Hibernate пишет в консоль ворнинг, который легко пропустить. Лучше сделать так:
🌷 Ставим свойство
spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true

🌷 Запускаем интеграционные тесты
🌷 Смотрим, где падают исключения

Как починить скрытую пагинацию?

Способов много, самое простое - добавить над списком @BatchSize. Либо поставить свойство
spring.jpa.properties.hibernate.default_batch_fetch_size=50

Тогда BatchSize будет по умолчанию применяться для всех списков.
Про другие решения и перфоманс читайте в этой статье на Хабре

Зачем Hibernate использует in-memory пагинацию?

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

Сразу возникает вопрос. Почему в таких случаях не сделать 2 запроса?
▫️ select * from posts limit 5;
▫️ Извлечь айдишники постов
▫️ select * from comments where post_id in (?,?,?,?,?);
▫️ Связать сущности

Решение выглядит просто, и вариант с BatchSize примерно так и работает. Почему оно не используется по умолчанию - непонятно.

Hibernate - яркий пример “протекающих абстракций”. Куча нюансов и проблем, о которых нужно знать. В документации по поводу in-memory панинации есть такая фраза:

Possibility of terrible performance is left as a problem for the client to avoid.
Проблема ужасного перформанса - это проблема клиента.

Вся суть хибернейта🤌

По возможности не тащите его в проект. Есть более удобные и прозрачные альтернативы. Например, Spring Data JDBC💚
2025/02/23 16:28:56
Back to Top
HTML Embed Code: