коротко

Синхронная интеграция — это «спросил и жду ответ прямо сейчас» (обычный REST-запрос): просто, но если сервис на той стороне тормозит или упал — ждёт и падает и ваша операция. Асинхронная — «отправил сообщение и пошёл дальше», ответ придёт потом, через очередь, событие, вебхук или вовсе файловый батч: устойчивее и масштабируемее, но сложнее. В асинхронном мире сообщение может прийти дважды, поэтому критичные операции (платёж, заказ) должны быть идемпотентными — повторный одинаковый запрос не создаёт второй платёж. Очереди (например, Kafka) развязывают сервисы во времени, но дают доставку «хотя бы один раз» — отсюда и нужна идемпотентность.

Самый дорогой баг в моей практике стоил клиенту двух списаний за один заказ. Платёжный сервис отвечал медленно, наш бэкенд не дожидался ответа и повторял запрос — а первый запрос на самом деле проходил. Деньги списывались дважды. Чинили не код отправки, а саму модель: научили платёж быть идемпотентным. Тогда я понял, что «синхронно или асинхронно» — это не про технологию, а про то, что произойдёт, когда что-то пойдёт не так. А пойдёт обязательно.

Синхронно и асинхронно: в чём разница

Разница — в ожидании. Если механизма REST вы ещё не видели, загляните в запись про REST API; здесь он будет примером синхронного вызова.

Синхронный вызов — клиент отправляет запрос и блокируется: стоит и ждёт ответ, прежде чем продолжить. Как телефонный звонок — вы не вешаете трубку, пока собеседник не ответит. Классический пример: REST-запрос «создай заказ — жду подтверждения».

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

sequenceDiagram
  participant A as Сервис A
  participant B as Сервис B
  participant Q as Очередь
  Note over A,B: Синхронно
  A->>B: запрос
  B-->>A: ответ (A всё это время ждёт)
  Note over A,Q: Асинхронно
  A->>Q: положил сообщение
  Q-->>A: принято (A сразу свободен)
  Q->>B: B заберёт, когда сможет

Схема показывает оба сценария. В синхронном случае сервис A отправляет запрос напрямую сервису B и блокируется до ответа — всё время, пока B думает, A простаивает и зависит от него. В асинхронном A кладёт сообщение в очередь и немедленно получает «принято», после чего свободен заниматься другим; сервис B заберёт сообщение из очереди тогда, когда будет готов, — может через миллисекунду, может через минуту. Главное отличие: в синхронном мире A и B жёстко связаны во времени, в асинхронном — развязаны через посредника.

Когда выбирать что

Это классический trade-off: простота против устойчивости и масштаба.

Синхронно — когда ответ нужен здесь и сейчас, чтобы продолжить. Пользователь вводит логин и пароль — без ответа «верно/неверно» дальше идти некуда. Когда операций немного и сервисы надёжны, синхронный REST проще: легче читать, отлаживать, понимать. Цена — связанность: если сервис на той стороне медленный или лежит, лежит и ваша операция.

Асинхронно — когда ответ не нужен немедленно, когда нагрузка скачет, когда одно событие интересно многим. Пользователь оформил заказ — ему не обязательно ждать, пока сработают начисление бонусов, отправка письма и обновление склада. Положили событие «заказ создан» в очередь, ответили пользователю «принято», а остальное досчиталось в фоне. Цена — сложность: появляются очереди, повторы, порядок сообщений, отслеживание «а оно вообще доехало».

Простое правило выбора

Нужен ответ, чтобы продолжить прямо сейчас, → синхронно. Можно ответить «приняли, сделаем» и досчитать в фоне → асинхронно. Если сомневаетесь — начинайте с синхронного: его проще понять и отладить, а на асинхронный перейдёте, когда упрётесь в нагрузку или связанность. Классический асинхронный случай — медленные вызовы к языковой модели: их не ждут синхронно в интерфейсе, а показывают прогресс.

Идемпотентность простыми словами

Идемпотентность — это свойство операции давать один и тот же результат, сколько бы раз её ни повторили. Нажать кнопку лифта пять раз — лифт приедет один раз: кнопка идемпотентна. А «снять 100 рублей со счёта» при пяти повторах снимет 500 — это не идемпотентно, и именно тут рождаются двойные списания.

Почему это вообще проблема: в сети запросы теряются и повторяются. Клиент отправил «создай заказ», не дождался ответа (сеть моргнула) и повторил. А первый запрос на самом деле дошёл. Без защиты получаются два заказа. С защитой — один.

Защита — идемпотентный ключ. Клиент генерирует уникальный идентификатор операции и прикладывает его к запросу, обычно в заголовке:

POST /payments
Idempotency-Key: 7f3e-a91c-2024-0042

{ "orderId": 123, "amount": 1500 }

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

Это вопрос аналитика, а не «магия бэка»

Идемпотентность не появляется сама. Кто-то должен решить: какие операции критичны (платёж, заказ, списание), кто генерирует ключ, как долго сервер его хранит, что вернуть на повтор. Это требования, и писать их — ваша работа. Если в спеке на платёж нет слова «идемпотентность» — спека дырявая.

Вебхуки: «не звони мне, я сам позвоню»

Бывает обратная задача: вам нужен ответ от внешнего сервиса, но не сразу, а когда у него что-то произойдёт. Платёж может обрабатываться минутами. Опрашивать сервис каждую секунду «ну как, готово?» — расточительно. Тут помогает вебхук.

Вебхук — это перевёрнутый запрос. Вместо того чтобы вы дёргали чужой API, вы регистрируете у него свой URL, и когда событие наступает, он сам присылает вам HTTP-запрос. «Платёж прошёл» — банк стучится на ваш /webhooks/payment и сообщает об этом. Это асинхронность через обычный HTTP: получатель узнаёт о событии в момент, когда оно случилось, а не когда в следующий раз догадается спросить.

Очереди и Kafka на пальцах

Когда сообщений много и важно ничего не потерять, между сервисами ставят очередь — посредника, который принимает сообщения от одних и отдаёт другим. Разберём на словаре Kafka, самой известной системы такого рода.

  • Продюсер (producer) — тот, кто пишет сообщения. Наш сервис заказов кладёт событие «заказ создан».
  • Топик (topic) — именованный поток сообщений, как канал. Например, топик orders. Сообщения в нём хранятся какое-то время, а не исчезают сразу после прочтения.
  • Консьюмер (consumer) — тот, кто читает. Сервис бонусов и сервис уведомлений независимо читают топик orders и каждый делает своё.
flowchart LR
  P["Продюсер: сервис заказов"] -->|"заказ создан"| T["Топик: orders"]
  T --> C1["Консьюмер: бонусы"]
  T --> C2["Консьюмер: уведомления"]
  T --> C3["Консьюмер: склад"]

На схеме сервис заказов (продюсер) пишет одно событие «заказ создан» в топик orders. Из этого топика событие независимо забирают три консьюмера — сервисы бонусов, уведомлений и склада, — и каждый делает свою часть работы, не мешая остальным и не зная друг о друге. В этом сила очереди: один отправитель, много получателей, все развязаны. Захотим завтра добавить четвёртый сервис — подпишем его на тот же топик, и отправителя менять не придётся. Когда всё общение системы строят вокруг таких фактов-событий, это называют событийной архитектурой.

Две модели потребления: каждому копию или одному из равных

Тут есть тонкость, на которой путаются даже опытные. «Много консьюмеров читают топик» — это две разные модели, и важно не смешивать их в спеке.

  • Fan-out (pub/sub) — каждый подписчик получает свою копию сообщения. Именно это на схеме выше: бонусы, склад и письма — три независимых получателя, и каждому нужно своё событие «заказ создан». Один отправил — все узнали.
  • Work-queue (очередь задач) — одно сообщение берёт ровно один из равноправных воркеров, остальные его не видят. Так масштабируют обработку: поставили пять одинаковых консьюмеров — нагрузка делится между ними, каждое сообщение обрабатывается один раз. Не «все узнали», а «кто свободен, тот и сделал».

В Kafka обе модели — это разные consumer groups. Группа — это команда консьюмеров с общим именем. Сообщение из топика доезжает до каждой группы (это и есть fan-out: бонусы, склад, письма — три разные группы, каждой своя копия), но внутри одной группы его обработает только один консьюмер (это work-queue: пять воркеров в группе «обработка заказов» делят работу). Хотите «все узнали» — заводите отдельные группы; хотите «поделить нагрузку» — добавляйте консьюмеров в одну группу.

At-least-once и почему без идемпотентности нельзя

Очереди вроде Kafka обычно гарантируют доставку «хотя бы один раз» (at-least-once): сообщение точно не потеряется, но в редких случаях может прийти дважды — например, консьюмер обработал его, но не успел подтвердить, и очередь прислала повторно. Поэтому асинхронность и идемпотентность ходят парой: раз сообщение может задвоиться, обработчик обязан быть готов к повтору. Это не теория — это про те самые двойные списания.

Порядок сообщений: глобального порядка нет

Частое заблуждение: «положили события в очередь — они и придут в том же порядке». В распределённой очереди это неправда. Kafka делит топик на партиции, и порядок гарантируется только внутри одной партиции. Между партициями глобального порядка нет: событие «оплата» из партиции 2 может прийти раньше «заказ создан» из партиции 1, хотя случилось позже. Лекарство — ключ партиционирования: связанные события кладут с одним ключом (например, по orderId), и Kafka гарантирует, что они лягут в одну партицию и сохранят порядок. Поэтому в спеке на событийную интеграцию всегда отвечайте на вопрос «по какому ключу события должны идти по порядку» — иначе порядок развалится молча и на проде.

Откуда это взялось

Термин webhook придумал Джефф Линдсей в 2007 году — как идею «обратных вызовов для веба», когда сервис сам уведомляет подписчиков о событии. Kafka родилась внутри LinkedIn около 2011 года: компании нужно было прокачивать гигантские потоки событий между десятками сервисов, и обычные очереди не тянули. Решение отдали в открытый код, и сегодня Kafka — фактический стандарт для потоков событий. Обе технологии решают одну боль — развязать сервисы во времени, — но с разных сторон: вебхук для «один отправитель, один-два получателя по HTTP», очередь — для «много сообщений, много получателей, ничего не терять».

Как это спрашивают на собесе

«Когда синхронно, а когда асинхронно?» — Ждут не перечисление технологий, а критерий. Нужен ответ прямо сейчас, чтобы продолжить (логин, проверка остатка, оплата на кассе), → синхронно. Достаточно сказать «приняли, сделаем», а результат досчитается в фоне (начисление бонусов, рассылка, обновление склада), → асинхронно. Джуну достаточно дать этот критерий и пример. От мидла ждут, что он назовёт цену асинхронности: повторы, отсутствие глобального порядка, мониторинг «доехало ли».

«Какие гарантии доставки бывают?» — at-most-once (доставим максимум раз, можем потерять), at-least-once (доставим хотя бы раз, можем задвоить), exactly-once (ровно один раз). Граница джун/мидл проходит здесь: мидл честно говорит, что exactly-once в распределённой системе дорогой и хрупкий, на практике берут at-least-once плюс идемпотентность на обработчике — это дешевле и надёжнее, чем гоняться за «ровно один раз».

«Зачем вообще очереди?» — Три ответа, которые ждут: развязать сервисы во времени (получатель может лежать, отправитель работает дальше); сгладить нагрузку (пик запросов копится в очереди, а не валит обработчик); fan-out — одно событие раздать многим получателям без изменений на стороне отправителя.

«Что такое идемпотентный ключ?» — Уникальный идентификатор операции, который клиент прикладывает к запросу (обычно в заголовке Idempotency-Key). Сервер по нему распознаёт повтор: новый ключ — выполняет и запоминает результат, тот же ключ — возвращает уже посчитанный результат, не выполняя заново. Так повтор из-за сбоя сети не создаёт второй платёж.

Частые вопросы

Что такое идемпотентность простыми словами?

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

Когда использовать очередь, а когда REST?

REST (синхронно) — когда ответ нужен немедленно, чтобы продолжить: логин, проверка, чтение данных. Очередь (асинхронно) — когда ответ не нужен сразу, нагрузка скачет или одно событие интересно нескольким сервисам: оформление заказа с фоновыми начислениями, рассылками, обновлением склада. Правило: нужен ответ прямо сейчас → REST; можно ответить «приняли» и досчитать в фоне → очередь.

Что такое вебхук (webhook)?

Вебхук — это перевёрнутый запрос: вместо того чтобы вы опрашивали чужой сервис «готово ли?», вы регистрируете у него свой URL, и сервис сам присылает вам HTTP-запрос, когда наступает событие. Например, платёжный провайдер стучится на ваш адрес, когда платёж прошёл. Это способ узнавать о событиях в момент их наступления, не дёргая чужой API в цикле.