Java: fill the gaps
12.9K subscribers
7 photos
215 links
Привет! Меня зовут Диана, и я занимаюсь разработкой с 2013. Здесь пишу просто и понятно про джава бэк

🔥Тот самый курс по многопочке🔥
https://fillthegaps.ru/mt

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Почему однопоточный Redis такой быстрый?

В прошлом посте предложила вам задачку: сравнить Redis и велосипедик на основе ConcurrentHashMap + Spring MVC.

ConcurrentHashMap — многопоточный, и вроде должен быть лучше. Но именно однопоточный Redis является базовым выбором для кэша.

Как однопоточный Redis справляется с нагрузкой?

Секрет в том, как он работает с запросами. Есть 2 основные модели:

🌊 Каждый запрос обрабатывается в своем потоке (thread per request).

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

У каждого потока свой стэк, переменные изолированы. Код легко писать, читать и дебажить. Идеальный вариант для сложных энтерпрайзных задач!

Но есть недостаток - число запросов в работе ограничено числом потоков в ОС. Обычно это несколько тысяч.

Из-за этой модели наш велосипед и проигрывает:
😒 Миллионы запросов просто не дойдут до ConcurrentHashMap, максимум несколько тысяч.
😒 Прочитать и записать в мэп - простые операции. Отправлять таких малышей в отдельный поток - как забивать краном гвозди. Очень большие накладные расходы на каждый запрос.

Redis использует другую модель:

🏃 EventLoop - малое число потоков бешено переключаются между запросами. В работу можно взять миллионы запросов!

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

Поэтому Redis и побеждает наш велосипед: возни с потоками нет, ограничений на запросы нет. Вся мощь процессора уходит на полезную работу, поэтому даже один поток справляется с большим объемом задач.

Можно ли взять лучшее из двух миров? Использовать многопоточность вместе с EventLoop?

Можно! Один поток Redis не использует все доступные ядра процессора, поэтому добавить десяток потоков - вполне рабочая идея.

Такую схему используют KeyDB и DragonflyDB. На сайте публикуют бенчмарки, где они обходят Redis в 5-25 раз. 25 раз звучит слишком мощно, но про 5-10 раз можно верить.

Почему чаще используется Redis, а не более быстрые альтернативы?

Потому что Redis появился в 2009, используется на сотнях проектов и закрепился в сознании как базовое решение для кэша. Подводные камни известны, инфраструктура налажена, куча статей и докладов.

KeyDB и DragonflyDB - свежие БД пирожки. Один вышел в 19 году, другой в 22. На конференциях особо не светились, громких кейсов внедрения пока нет.

Энтерпрайз мир тяжело принимает новые технологии. Плюс не всегда нужно лучшее решение, иногда достаточно хорошего😊
🔥234👍6922👎8
Требуется Java эксперт

Последнее время плотно занимаюсь новым проектом. Цель проста - помочь джавистам профессионально расти и быть на коне в текущих сложных условиях.

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

Для реализации мне нужны помощники, поэтому открываю вакансию: Java эксперт. Ищу людей, которые
Знают и любят джава разработку
Работали минимум на 3 разных проектах и видели всякое
Критически мыслят, видят логические несостыковки
Умеют работать с первоисточниками и проверять информацию. Не боятся простыни текста на английском
Большой плюс, если вы часто проводите собеседования и видите, где у людей случаются затыки

Что нужно делать: писать статьи по моему ТЗ. Сначала проработаем популярные темы типа HashMap, ACID, N+1, потом углубимся в другие нужные вещи.

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

Как присоединиться:
▫️ Заполнить анкету с небольшим тех.скринингом
▫️ Если все ок, я пришлю вам на почту оплачиваемое тестовое задание

Если вам интересно в этом поучаствовать, заполняйте анкету, не стесняйтесь. Если у вас есть знакомый, который подходит под описание, пришлите ему этот пост❤️
🔥81👍2821👎19
Принципы SOLID и групповой транс

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

Типичный диалог на собеседовании выглядит так:
👨‍💼: Расскажите про SOLID
👱‍♂️: [расшифровка каждой буквы]
👨‍💼: Используете в работе?
👱‍♂️: Конечно!


Хорошо, если на этом все заканчивается. Но бывает и продолжение: "расскажите принципы своими словами". На этом месте даже опытные и умные люди впадают в транс. Например, кандидат описывает принцип Open-closed:

👱‍♂️: Чтобы добавить новый метод, нужно не менять существующий класс, а создать новый
👨‍💼 : Угу, давайте дальше

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

Dependency Inversion часто объясняют как "нужно всё делать через интерфейс".
Map<String, String> map = new HashMap<>();

Это считается реализацией DI? Чем отличается от инкапсуляции? Для каждого класса нужно создавать интерфейс? Работа с любым классом напрямую — это нарушение принципа? Почему в названии есть инвершн, что инвертируется-то?

Вопросы здравые, но люди в трансе:) У других абстрактных тем вроде принципов ООП или паттернов такого эффекта нет.

SOLID — самая унылая тема на собеседовании. Формальная, абстрактная и скучная для всех сторон. Собеседование длится всего час-полтора, нет никакого смысла тратить время на ритуальный обмен фразами.

Применяются ли принципы SOLID на практике?

▫️ Single Responsibility - да. Что неудивительно, это самый понятный и простой принцип.

▫️ Open-closed, Liskov и Interface Segregation тесно связаны с наследованием и сложными иерархиями классов. Для бизнес логики большинства систем это не очень актуально. Там максимум интерфейс и несколько реализаций.

▫️ Dependency Inversion - самый непонятный и недооцененный принцип. За 10+ лет в разработке я встречала мало людей, которые его поняли. Это не просто "делаем всё через интерфейс". Можно реализовать его даже без интерфейсов, суть вообще в другом.

К четвергу напишу пост и подробно объясню🔥
🔥369👍6526👎17
Dependency Inversion

Сегодня расскажу, в чем суть принципа DI из SOLID, и почему с ним так много проблем. Вспомним формулировку:

✍️ Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
✍️ Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Ставь огонёк, если даже не вчитывался в этот набор слов😊

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

Роберт Мартин, он же дядюшка Боб, в целом молодец, но конкретно здесь плохо донёс свою идею. В оригинальной статье всё очень сумбурно. Интерфейсы добавляются на каждом шагу и отвлекают от сути dependency inversion. Ничего удивительного, что большинство людей не поняли, о чём речь.

Дядюшка Боб не смог, тетя Диана объяснит🤌

Все очень просто. У каждого класса есть своя область ответственности, single responsibility:
🔴 Кнопку нажимают, и она отправляет сигнал об этом. Ничего больше
💡 Лампа включается и выключается, когда ей говорят. Ничего больше
💏 Взаимодействие этих сущностей должно быть описано отдельно

Где здесь инверсия:

В нашем мире все описывается последовательно, причина -> следствие. Нажали кнопку -> включилась лампа. Перекладывая на код, в классе Кнопка будет поле Лампа:
public class Button
   private Lamp lamp;
   public void push() {
       // включить/выключить лампу
   }
}

В мире с Dependency inversion Кнопка и Лампа ничего не знают друг о друге, логика взаимодействия описана в классе Электросхема. Она ловит сигналы от Кнопки и отправляет команды Лампе:
public class Circuit {
    private Button button;
    private Lamp lamp;
    public void process() {
        // получить сигнал от кнопки
        // сказать лампе включиться / выключиться
    }
}

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

Интерфейсы можно добавить, чтобы жонглировать реализациями, но это не главное. Dependency inversion говорит о том, что логика взаимодействия компонентов должна находиться НЕ в самих компонентах.

Кстати, если перейти с уровня классов на уровень контроллеры/сервисы, получится Clean architecture. В основе та же идея: компоненты отдельно, взаимодействие отдельно.

Всё очень просто❤️
🔥297139👍77👎47
В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇

Какая проблема возможна в этом коде?
🔥36👍10
Популярная ошибка в блокировках, часть 1

Недавно делала ревью одного сервиса и нашла там ошибки, связанные с блокировками. Я встречала подобные ошибки и на других проектах, поэтому давайте разберём их вместе, будет полезно:)

Исходная цель: распределить задачи между сервисами. Решается очень просто:
✍️ Складываем задачи в отдельную таблицу
🙋 Каждый сервис выбирает из таблицы одну строку и ставит на неё блокировку. Благодаря блокировке другой сервис не может взять эту задачу в работу.

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

🤔 Почему?

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

Что делать:
Поставить аннотацию Transactional над Scheduled методом. Тогда блокировка держится во время всей работы над задачей
Плюс: просто реализовать
Минус: если обработка сложная, мы долго и нерационально держим соединение с базой

Альтернатива — отказаться от блокировок. Берём задачу в работу — меняем статус в БД на "in progress". Делается одним запросом с помощью RETURNING:
UPDATE tasks SET status = 'in progress'
WHERE status = 'not processed'
RETURNING id, …;

Плюс: нет длинной транзакции
Минус: если сервис упадёт, задача останется в БД со статусом "в работе", и в итоге не будет обработана. Нужно дополнительно следить за такими ситуациями.

Итого

Блокировки работают до конца работы транзакции. Если полагаетесь на блокировки - не отпускайте их раньше времени.

Это просто, но в большой кодовой базе легко упустить этот момент. Будьте внимательнее❤️ Вторую проблему опишу в следующем посте!
🔥193👍6940👎17
Продолжение прошлого вопроса. В БД складываются задачи, которые затем распределяются между несколькими сервисами. Сервис извлекает задачи и обрабатывает их примерно так👇

Над Scheduled задачей добавилась аннотация Transactional.

Какая проблема возможна в этом коде?
🔥24👍4
Популярная ошибка в блокировках, часть 2

Продолжим разбирать ошибки при распределении задач. В прошлом посте мы разобрали, что не нужно отпускать блокировку слишком рано, иначе одну задачку возьмут несколько сервисов. Сегодня разберём следующую проблему:

Задачи в разных сервисах не обрабатываются параллельно

Почему?

🧑‍💻 Сервис 1 выполняет запрос "найди задачу для обработки", получает строку 1 и блокирует её
🧑‍💻 Сервис 2 выполняет такой же запрос, получает в результате ту же строку. Но блокировку поставить не может, так как строка уже заблокирована.
💅 Сервис 2 ждёт, пока блокировка снимется

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

Не самая эффективная командная работа🙈

Чтобы исправить ситуацию, сервисы должны пропускать заблокированные строки и брать себе задачу из "свободных". При взятии блокировки нужно добавить SKIP LOCKED.

Но средствами Spring Data SKIP LOCKED не сделать, поэтому напишем SQL запрос над методом в репозитории:
@Query(value = "SELECT * FROM outbox 
WHERE is_done = false
ORDER BY id ASC LIMIT 1
FOR UPDATE SKIP LOCKED",
nativeQuery = true
)

Optional<OutboxEntry> findFirstByIsDoneFalseOrderByIdAsc();

Теперь задачи обрабатываются параллельно несколькими сервисами🥳

P.S. Очень рада, что в прошлом опросе было много правильных ответов, вы умнички🥰
🔥220👍5945👎5
Критикую Object

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

Предлагаю вам взглянуть по-новому на класс Object. Удивиться, насколько он плох с точки зрения API🙈

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

Возьмём метод hashCode. Для чего объекту нужен хэш?

Для некоторых алгоритмов и структур данных. Чаще всего хэш используется в HashMap или HashSet.

Но как часто объект становится ключом в HashMap? Часто ли в бизнес-логике используются хэши? Нужен ли hashCode каждому классу?

Вряд ли.

А ещё есть контракт equals/hashcode. Он висит невидимой тенью (хорошей практикой) над каждым разработчиком. Хочешь сравнивать объекты через equals — не забудь определить hashСode. Даже если хэш в коде не нужен.

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

Альтернативный путь использует compareTo. Его нет в наборе методов обжекта. Чтобы сравнить объекты или сложить их в TreeSet, мы либо реализуем Comparable, либо передаём логику сравнения через Comparator.

Такой подход отлично подошёл бы для хэша!

Захотим использовать объект внутри хэш-структуры — реализуем интерфейс или передадим лямбду в HashMap. Захотим посчитать хэш для контрольной суммы — просто реализуем метод, где это необходимо.
Не думаем про хэш, когда он не нужен
Компилятор укажет, где хэш не определен, но используется

В других языках, кстати, нет по умолчанию всеобщего хэша. В С++ используется compareTo-подход. В Go не всё может стать ключом, зато хэш для таблицы автоматом считается по всем полям. Мне оба варианта нравятся, работа с хэшем более явная и предсказуемая👌

А что с другими методами Object?

К ним тоже вопросики. Нужны ли каждому классу методы wait и notify? Почему clone такой странный? Зачем нужен equals по умолчанию, если внутри просто ==? Хорошо хоть finalize отметили как deprecated.

Польза базовых hashCode, equals, toString похоже чисто синтаксическая. Показать, что именно можно переопределить.

Итого: лишние методы, слабые варианты по умолчанию. Поэтому общая оценка Object с точки зрения API - слабая троечка

PS
В целом java и её вселенная — конфетка. Критикую любя и чисто в образовательных целях. Посмотреть на привычные вещи под другим углом и подумать "как можно иначе" всегда интересно
👍150🔥5444👎24
Java 25: новый формат конструктора

Сегодня расскажу про новую фичу в осенней джаве. Суть простая - this и super не обязательно должны идти первой строкой в конструкторе.

Зачем это нужно? Чтобы упростить валидацию.

Сейчас, чтобы добавить проверку аргументов, приходится оборачивать аргументы в методы:
class Employee extends Person {
  private static int verifyAge(int value) {
  if (age < 18)
    throw new IllegalArgumentException(...);
    return value;
  }

  Employee(int age) {
     super(verifyAge(age));
  }
}


С новым JEP эти костыли не нужны, нужные проверки пишем в начале конструктора:
class Employee extends Person {
  Employee(int age) {
  if (age < 18)
   throw new IllegalArgumentException(...);
  super(age);
  }
}


Области кода вокруг this/super называются очень литературно: пролог и эпилог🥰
public Person {
   // prologue
   super();
   // epilogue
}

В пролог нельзя вставить любой код:
Нельзя обращаться к переменным родителя
Нельзя вызывать нестатические методы
Нельзя вызвать return
Можно присвоить поля текущего класса

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

Ещё из интересного:

1️⃣ В JVM не пришлось ничего менять

Потому что правила "this обязательно первый" в JVM нет. Это ограничение только на уровне языка, чтобы упростить работу компилятора:)

2️⃣ Меняется ответ на частый собесный вопрос "в каком порядке инициализируются переменные". Раньше порядок для нестатических полей был такой:

Поля Parent - Конструктор Parent - Поля Child - Конструктор Child

В Java 25 поля наследника можно инициализировать ДО вызова конструктора родителя:
Employee(int age, String officeID) {
   this.officeID = officeID;
   super(age);
}

Общая схема с этими прологами-эпилогами очень усложняется.

3️⃣ Фича называется Flexible Constructor Bodies. Я не смогла придумать адекватный перевод, поэтому очень интересно, как её переведут в статьях-обзорах:)
🔥106👍5222👎6
Как прогреть кэши в Spring Boot?

и вообще сделать что-то на старте приложения?

Обычно подобные операции делают в методе с PostConstruct или ловят событие ApplicationReadyEvent. Но у таких вариантов есть существенный минус.

Если "прогрев" находится в PostConstruct, как отключить его в тестах? Можно добавить флажок, сделать подкласс и заменить его в тестовом конфиге, но это не всегда помогает и часто выглядит как костыль.

Расскажу более аккуратный способ "прогреть кэши". Однажды точно пригодится💯

Смотрите, SpringApplication.run(...) возвращает полностью готовый контекст. Можно достать из него компонент и вызвать нужный метод "прогрева". В коде выглядит так:
@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ApplicationContext ctx = SpringApplication.run(MainApplication.class, args);
    AccountService accService = ctx.getBean(AccountService.class);
    accService.loadDictionary();
  }
}

В чём плюс?

В интеграционных тестах с @SpringBootTest метод main не запускается. Соответственно, код внутри не выполняется. Нет костылей вокруг PostConstruct, всё чисто и красиво❤️‍🩹

Когда в тестах все же нужен "прогрев", добавляем параметр "использовать main метод":
@SpringBootTest(useMainMethod = SpringBootTest.UseMainMethod.ALWAYS)

Итого. Если код должен выполниться после старта, но будет мешаться в тестах — пишите его в main. Очень полезный приём🔥
🔥219100👍40👎32
Вопрос на понимание транзакций.

Есть таблица c твитами. На каждый твит можно ответить, id "исходного" твита сохраняется в поле parent_id:
CREATE TABLE tweets (
id BIGINT ... PRIMARY KEY,
text VARCHAR(255) NOT NULL,
parent_id BIGINT REFERENCES tweets(id)
);


Код сохранения твита примерно такой:
public Tweet saveTweet(Tweet tweet) {
// проверяем, что родитель существует
if (tweet.getParentId() != null) {
boolean parentExists = tweetRepository.existsById(tweet.getParentId());
if (!parentExists) {
throw new IllegalArgumentException("Parent not exists");
}
}
return tweetRepository.save(tweet);
}


Какую аннотацию ставим над методом saveTweet?
👍254
Какую аннотацию ставим над методом saveTweet?
Anonymous Poll
27%
@Transactional(isolation = Isolation.REPEATABLE_READ)
9%
@Transactional(isolation = Isolation.SERIALIZABLE)
31%
Никакую
👍19🔥63👎1
Сохранение сущностей внутри Transactional

Часто встречаю такую логику при работе с Spring Data:

💁: Если в одном методе несколько обращений к БД, надо ставить Transactional.

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

Итак, вот у нас код. Внутри метода несколько обращений к БД. Что может пойти не так?

Допустим, пользователь написал провокационный твит, потом одумался и удалил его. Но другие пользователи успели увидеть и настрочили гневных ответов. Что делать, если родительская сущность удалится после проверки existsById? Никто этому не мешает, удаление происходит в другом запросе.

Аналитик говорит: если родительский твит удалён, гневные ответы не сохраняем. Пользователю шлём сообщение "не получилось".

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

Транзакция - это не аналог synchronized, она не запрещает другим транзакциям менять данные.

Задача решается на уровне ограничений в БД. Указываем, что parent_id - это ссылка на другую запись. Если на момент вставки не будет поля с таким id, получим DataIntegrityViolationException. Ну или concurrent update при уровне изоляции Repeatable Read и выше.

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

Наблюдение из опыта: в Spring Data аннотация ставится за полсекунды, и осмысление иногда занимает столько же:) И либо Transactional вообще не ставят, и за счёт низких нагрузок проблем не возникает. Либо ставят на каждый чих и упираются в проблему с соединениями для вроде бы небольшой нагрузки.

Не надо так, будьте внимательнее с транзакциями в своем коде и на код-ревью❤️
👍10842🔥35👎29
Вопрос для матёрых бекендеров. Укажите элемент, который отличается от остальных в группе:
Anonymous Poll
33%
2181
14%
5432
16%
6379
9%
9092
29%
9200
🔥28👍9👎6