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

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

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

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

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

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

🤔 Почему?

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

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

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

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

Итого

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

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

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

Какая проблема возможна в этом коде?
🔥20👍3
Популярная ошибка в блокировках, часть 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. Очень рада, что в прошлом опросе было много правильных ответов, вы умнички🥰
🔥209👍5944👎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 и её вселенная — конфетка. Критикую любя и чисто в образовательных целях. Посмотреть на привычные вещи под другим углом и подумать "как можно иначе" всегда интересно
👍142🔥5042👎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. Я не смогла придумать адекватный перевод, поэтому очень интересно, как её переведут в статьях-обзорах:)
🔥98👍4921👎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. Очень полезный приём🔥
🔥201100👍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?
👍222
Какую аннотацию ставим над методом saveTweet?
Anonymous Poll
27%
@Transactional(isolation = Isolation.REPEATABLE_READ)
9%
@Transactional(isolation = Isolation.SERIALIZABLE)
32%
Никакую
👍18🔥63👎1
Сохранение сущностей внутри Transactional

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

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

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

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

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

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

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

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

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

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

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

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

Магические числа в вопросе выше — это порты. В этом посте расскажу, зачем они нужны и что с ними не так.

Немного теории. Компьютеры обмениваются данными по модели OSI. Каждый запрос проходит через 7 этапов(уровней) на компьютере отправителя, передаётся по сети, потом проходит те же этапы на компьютере получателя в обратном порядке. Упрощённо, процесс выглядит так:

▫️Пользователь шлёт select запрос в БД
▫️▫️Запрос разбивается на TCP пакеты
⚡️⚡️⚡️Пакеты передаются по сети и прибывают на сервер БД
▫️▫️TCP пакеты собираются в select запрос
▫️Запрос выполняется в базе данных

По сети передаются миллиарды пакетов вперемешку. Чтобы понять, какие пакеты к чему относятся, в каждый пакет добавляется поле "порт". Получатель собирает пакеты с полем 5432 и преобразует их в SQL запрос.

Хорошая абстракция скрывает детали реализации и упрощает жизнь пользователю. Если пользователю приходится учитывать детали реализации, такая абстракция называется протекающей (leaky abstraction).

Порт — яркий пример протекающей абстракции.

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

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

Есть другие транспортные протоколы и схемы адресации, которым достаточно только IP-адреса. Но стек TСP/IP самый распространённый, хорошо работает, да и к портам все привыкли:)

Ответ на вопрос перед постом. Основная группа — это порты сервисов по работе с данными: 5432 - Postgres, 6379 - Redis, 9092 - Kafka, 9200 - ElasticSearch. Порт 2181 использует Zookeeper, он занимается координацией сервисов👩‍✈️
🔥105👍41👎2713
LinkedHashMap и проектирование API

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

Люблю разбирать классы в JDK. Они используются каждый день и интересны сами по себе. А ещё на их примере удобно объяснять разные концепции, хорошие и плохие практики.

На этой неделе расскажу пару неочевидных моментов на основе класса LinkedHashMap.

Начнём с базы. LinkedHashMap - это HashMap, внутри которого есть связный список. По умолчанию список сохраняет порядок вставки элементов:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
map.put(1, 1);
map.put(2, 2);
int lastKey = map.sequencedKeySet().getLast(); // 2


Если в конструкторе передать accessOrder=true, список запоминает порядок извлечения элементов. Вызов get(1) отправляет 1 в конец списка:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(16, 0.8f, true);
map.put(1, 1);
map.put(2, 2);

map.get(1);

int lastKey = map.sequencedKeySet().getLast(); // 1


С теорией закончили, переходим к интересному.

В первую очередь, бросается в глаза флажок в конструкторе. Гораздо симпатичнее в параметрах выглядел бы enum. Что-то вроде ELEMENT_ORDER.INSERT. Подробно бэд пректис с флажками и альтернативы разбирала тут.

Второй момент касается проектирования. Зачем вообще нужен LinkedHashMap с accessOrder=true?

Документация пишет, что это отличная база для LRU кэша. С первого взгляда похоже на правду, но есть пара нюансов:
▫️ У LinkedHashMap нет ограничений на размер. У кэша — есть
▫️ С кэшами работают много потоков. LinkedHashMap - не потокобезопасен, единственный вариант для корректной работы — synchronized обёртка:
Map map = Collections.synchronizedMap(new LinkedHashMap(…)) 

Получится кэш с пропускной способностью в один поток😐

В итоге:
В чистом виде LinkedHashMap с accessOrder=true нужен либо никому, либо в редких случаях
В качестве LRU кэша (как предлагается в документации) класс использовать сразу не получится. Либо доделывать, либо взять уже готовые и более эффективные реализации кэша.

Подобные опции в API - лишние.

Хорошие библиотеки и фреймворки состоят из двух частей: базовые многофункциональные элементы + удобные методы для популярных кейсов. Так достигается баланс между простотой и гибкостью.

Хороший пример: экзекьюторы

Базовый элемент для экзекьютора — класс ThreadPoolExecutor. В конструкторе 5 параметров, можно переопределить методы. Есть готовые варианты, которые подойдут для большинства задач:
Executors.newFixedThreadPool(int nThreads) 
Executors.newSingleThreadExecutor()
Executors.newCachedThreadPool()

Всё вместе - приятное и удобное апи.

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

Ответ на вопрос перед постом: с флажком true запоминается порядок извлечения элементов. Подписчики канала - умнички независимо от каких-то флажков🥰
🔥109👍3531👎2
LinkedHashMap: наследование и композиция

Сегодня на примере LinkedHashMap очень чётко покажу проблему наследования. Ни в одной теоретической статье не найдёте такого наглядного примера. Просто бриллиант💎

Действующие лица - классы HashSet, HashMap, LinkedHashSet и LinkedHashMap. В них много общего кода, и организовать нужные связи — задача со звёздочкой.

В JDK эту задачу решили не лучшим образом. Как раз по вине наследования.

Вернёмся в 1998 год. Я смотрела Сейлор Мун, кто-то из подписчиков даже не родился. Ещё в тот год вышла java 1.2. В пакете java.util были 2 всем знакомых класса: HashMap и HashSet.

HashSet сделан на базе HashMap примерно так:
public class HashSet implements Set {
    HashMap map;
    public HashSet() {
        map = new HashMap<>();
    }
    ...
}

Прошло 4 года. В java 1.4 добавились Linked* реализации:
▫️ LinkedHashMap стал наследником HashMap. Тут всё сложилось удачно
▫️ LinkedHashSet стал наследником HashSet. И здесь не всё гладко.

У HashSet внутри HashMap, доступа снаружи к нему нет. А внутри LinkedHashSet должен быть LinkedHashMap. Как заменить объект внутри родителя без изменения существующих методов?

Решение в итоге ужасное. В HashSet добавили такой package private конструктор:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

поле dummy нужно, чтобы этот конструктор отличался от уже существующих.

Только вдумайтесь: в родителя добавили специальный конструктор для конкретного потомка. С его деталями реализации!

Код получился бы чище, если вместо наследования от хэшсета использовать композицию:
class LinkedHashSet implements Set {
  private LinkedHashMap map;
  public LinkedHashSet() {
    this.map = new LinkedHashMap<>();
  }
  ...
}

В HashSet нет лишнего
😑 Нужно скопировать кучу мелких методов типа add, size, contains

На примере LinkedHashSet чётко видна проблема наследования.

У родителя по всем заветам инкапсуляции скрыта реализация. Если наследование изначально не предусмотрено, к внутренним полям родителя нет прямого доступа. Код сложнее переиспользовать, и появляются странные конструкции.

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

Композиция и в моменте, и в перспективе гораздо удобнее. Но 30 лет назад это было не так очевидно.
🔥102👍4215
Аннотация Scheduled

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

🚨 Ситуация 1: у сервиса несколько задач по расписанию

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

Поэтому если один сервис выполняет несколько задач по расписанию, увеличьте размер пула параметром:
spring.task.scheduling.pool.size=5

Задачки будут летать параллельно и не блокировать друг друга👯‍♀️

🚨Ситуация 2: у сервиса несколько экземпляров
а задача по расписанию должна выполниться один раз.

Какие решения я видела:
🕳 Отдельный сервис для скедьюлд задач
🕳 Синхронизация через базу или ShedLock, чтобы решить, какой сервис выполнит задачу. На одном проекте для этой цели использовался leader election в Zookeeper🙈

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

Кто этот внешний компонент? Кто угодно, в инфраструктуре полно инструментов для регулярных задач. Начиная от CronJobs в Kubernetes до баш скрипта с crontab.

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

Я такое разгребала неоднократно, поэтому совет от души: когда добавляете задачу по расписанию, учтите 2 момента из этого поста. Сэкономите проекту десятки человекодней🔥
🔥102👍4516👎3