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

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

Комплименты, вопросы, предложения: @utki_letyat
Download Telegram
Популярная ошибка в блокировках, часть 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🔥5344👎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. Я не смогла придумать адекватный перевод, поэтому очень интересно, как её переведут в статьях-обзорах:)
🔥105👍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. Очень полезный приём🔥
🔥216100👍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?
👍244
Какую аннотацию ставим над методом 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
Протекающая абстракция

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

Немного теории. Компьютеры обмениваются данными по модели 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, он занимается координацией сервисов👩‍✈️
🔥115👍44👎2913
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 запоминается порядок извлечения элементов. Подписчики канала - умнички независимо от каких-то флажков🥰
🔥125👍4134👎3
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 лет назад это было не так очевидно.
🔥116👍4822👎1
Аннотация Scheduled

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

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

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

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

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

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

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

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

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

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

Я такое разгребала неоднократно, поэтому совет от души: когда добавляете задачу по расписанию, учтите 2 момента из этого поста. Сэкономите проекту десятки человекодней🔥
🔥156👍5823👎7
В main методе запустили 10 экземпляров Thread, 3 из них - с daemon статусом. Внутри JVM работают 5 daemon потоков для служебных нужд (GC и проч). Сколько потоков операционной системы занято программой?
Anonymous Quiz
11%
7
15%
8
16%
10
40%
15
15%
16
2%
24
🔥358👍7
Как устроена многопоточность в разных языках

Если хотите сделать доклад для конференции — сравните подходы к чему-нибудь в разных языках. Такой доклад можно прочитать на конференциях всех упомянутых языков. Устроить турне по городам, почувствовать себя звездой🥰

Однажды так и сделаю, но пока просто напишу пост, как разные языки работают с потоками. Рассмотрим Java, Python, JavaScript и Go.

Немного базы:
▫️ Если у процессора 8 ядер, в каждый момент времени выполняется максимум 8 задач
▫️ Задачи выполняются потоками операционной системы (ОС). Планировщик ОС распределяет процессорное время между потоками ОС
▫️ Процесс — это запущенная программа. У каждого процесса своя память и ресурсы. Другие процессы не имеют к ним доступа.
▫️Внутри процессов есть потоки. Каждый поток выполняет свою задачу. Потоки могут обмениваться данными через общую память процесса.

Python и JavaScript

В каждом процессе (программе) используется один поток операционной системы.

Потоки в этих языках - это способ логического разделения задач. Например, запрос 1 выполняется в потоке Т1, запрос 2 — в потоке Т2. У каждой задачи своя область видимости и локальные переменные. Один поток ОС переключается между такими "потоками", и так создаётся иллюзия одновременного выполнения.

Один экземпляр сервиса нагружает только одно ядро процессора. Чтобы задействовать 8 ядер, запускают 8 экземпляров сервиса (процессов) + балансировщик.

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

Java (традиционная модель)

Потоки в джаве соотносятся с потоками ОС в отношении 1 к 1. Каждый экземпляр Thread жёстко связан с одним потоком в ОС. Планировщик ОС распределяет процессорное время. Если ядер 8, в каждый момент времени параллельно работают максимум 8 задач.

Простая инфраструктура
Общая память между потоками, обмениваться данными проще и быстрее
Шикарная библиотека java.util.concurrent
💔 Дополнительные сложности: гонки, дедлоки, проблемы с видимостью и атомарностью. Многопоточный код сложно тестировать и дебажить
💔 Число потоков и задач в работе ограничивается ОС

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

Go и Java с виртуальными потоками


Потоки соотносятся с потоками ОС как многие ко многим. Распределением потоков по ядрам ОС занимается сам процесс (JVM или Go runtime)

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

Что тут важно:

Говорят, что язык программирования не влияет на общую архитектуру. Квадратик микросервиса на схеме может быть написан на чем угодно. Это не совсем так. Синтаксис языка и правда не важен, а вот модель многопоточности напрямую влияет и на архитектуру, и на инфраструктуру.

Теорию рассказала, в следующем посте покажу конкретный пример🔥
🔥134👍3729👎9
Какой язык из перечисленных самый старый?
Anonymous Quiz
26%
Java
5%
JavaScript
9%
Ruby
33%
PHP
9%
C#
19%
Python
🔥365👎3
Зачем Redis для задач по расписанию?

На прошлой неделе писала про задачки по расписанию и аннотацию Scheduled. Сегодня расскажу интересный кейс, когда для такой задачи используется очередь Redis.

Типичная реакция джавистов: "Чего? Как? Зачем???"🤯

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

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

✍️ Основной процесс описывает задачу, которую нужно выполнить по расписанию
✍️ Отдельный сервис-планировщик следит, когда наступит указанное время
✍️ В нужный момент задача сериализуется и отправляется в очередь Redis
✍️ Сервис-исполнитель забирает задачу из очереди и выполняет
✍️ При необходимости результат отправляется обратно в Redis, и основной сервис его забирает

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

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

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

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

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

Зачем изучать подходы в других языках?

Как я писала в прошлом посте, модель многопоточности влияет на архитектуру и инфраструктуру. У каждого языка свои "стандартные решения". Если сервис написан на Python, надо подкручивать определенные настройки Redis. В JS модель многопоточности как в питоне, но задачи по расписанию чаще реализуют через crontab и Mongo.

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

И, конечно, инженерный интерес! У разных инструментов разные подходы, свои преимущества и ограничения. Разбираться, как и за счёт чего решаются задачи, оценивать трейдоффы и выбирать подходящее решение очень интересно. Такие задачки - мои самые любимые🥰
🔥89👍3514👎10
🔥14👍5