Перейти к содержимому

Основы

Как сохранить историю каждого заказа

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

Изучать с ИИ-ассистентом 🤖

Скопируйте промпт и вставьте его в чат с ИИ-ассистентом.

Ты — преподаватель курса «Потоковая обработка».
Тема: «Как сохранить историю каждого заказа»
О чём: Как ключ разделяет память о заказах, когда события многих заказов приходят вперемешку.
Текст статьи в Markdown: https://stream-processing-systems.github.io/ru/course/fundamentals/keeping-each-orders-story-together.md
Опирайся на эту статью. Сначала спроси, как мне удобнее: кратко, пошагово или глубоко с вопросами на понимание. Объясняй на примере из статьи и в её терминах. Если добавляешь сведения, которых в статье нет, явно это отмечай.

В нашем сервисе доставки продуктов обработчик (processor) ведёт в магазине список заказов, которые ждут ответа покупателя. Он помнит, ждёт ли заказ ответа: для вашего заказа ord-1042 — «да» после предложения в 10:04 и «нет» после вашего согласия в 10:06. Но магазин собирает много заказов одновременно. Красные яблоки закончились, поэтому в 10:05 сборщик другого заказа, ord-1337, тоже предлагает вместо них зелёные. Это событие (event) приходит между предложением по вашему заказу и вашим ответом.

События обоих заказов приходят из одного источника (source) вперемешку. Когда придёт ваше согласие, обработчик должен убрать из списка ваш заказ и оставить в нём ord-1337. Как ему не смешать память о разных заказах и как найти нужную память для каждого события?

Одно сохранённое «да» или «нет» не может описать два заказа. Если бы обработчик хранил один ответ на весь магазин, ваше согласие в 10:06 поменяло бы его на «нет», и ord-1337 ушёл бы из списка, хотя его покупатель ещё решает.

Поэтому обработчик хранит состояние (state) отдельно для каждого заказа, как маленькую таблицу, где у каждого заказа своя строка. Когда приходит событие, обработчик читает его order_id, находит строку этого заказа и меняет только её1. Значение, по которому определяется, к какой памяти относится событие, называется ключом (key). Здесь ключ — order_id.

Память для каждого заказа События двух заказов приходят вперемешку, и обработчик по идентификатору заказа в каждом событии меняет только строку этого заказа. ПАМЯТЬ ПО КЛЮЧАМ СОГЛАСИЕ, ORD-1042 КЛЮЧ ORD-1042 Источник события обоих заказов вперемешку Обработчик key = order_id ord-1042 ждёт: да → нет ord-1337 ждёт: да, без изменений

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

Вот как приходят события обоих заказов и что обработчик помнит после каждого:

ВремяСобытиеКлючord-1042 ждёт?ord-1337 ждёт?
10:04replacement_proposedord-1042ДаНет
10:05replacement_proposedord-1337ДаДа
10:06replacement_acceptedord-1042НетДа
10:08replacement_acceptedord-1337НетНет

В 10:06 обработчик меняет только строку ord-1042, а ord-1337 остаётся в списке. Событиям одного заказа не обязательно приходить подряд. Каждое событие несёт свой ключ, и ключ приводит обработчик к нужной памяти, сколько бы событий других заказов ни пришло между ними.

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

КлючКакие события делят памятьРезультат
event_idНикакие: у каждого события своя строкаВаше согласие не доберётся до «да», которое оставило предложение, и ваш заказ останется в списке
store_idВсе события всех заказов в store-418Ваше согласие поменяет ответ магазина на «нет» и уберёт из списка заодно и ord-1337
order_idВсе события одного заказаОтвет каждого заказа меняется только от его собственных событий

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

Ключ должен ещё и различать разные объекты. В нашем сервисе каждый номер заказа уникален во всех магазинах. Если бы каждый магазин нумеровал заказы сам, у двух магазинов мог бы оказаться заказ ord-1042, и их события попали бы в одну строку. Тогда ключ пришлось бы составить из двух полей: store_id и order_id вместе.

Ключ определяет, какую память меняет событие. Порядок, в котором приходят события, он не определяет. Записи могут приходить не в том порядке, в котором происходили описанные действия. Если запись о предложении, сделанном в 10:04, задержится, запись о вашем согласии в 10:06 может дойти до обработчика первой. Оба события всё равно попадут в строку ord-1042. Согласие поменяет её на «нет», хотя там и так было «нет». Затем опоздавшее предложение поменяет её на «да», и ваш заказ попадёт в список уже после вашего ответа. Сотрудники магазина увидят, что сборщик ждёт ответа, который вы уже дали.

В событиях записано время, когда всё произошло: 10:04 у предложения, 10:06 у согласия. Один из подходов — задерживать поступившие события и обрабатывать их в порядке возникновения, по записанному времени, а не по порядку получения. Но как долго обработчику их задерживать, если более раннее событие может быть ещё в пути?

  • Ключ — значение, по которому определяется, какие события относятся друг к другу и какую память меняет событие.
  • Обработчик хранит отдельное состояние для каждого ключа, поэтому события многих заказов могут приходить вперемешку, не смешивая их память.
  • Ключ следует из вопроса: по заказу, по магазину или по сочетанию полей, если одно поле не различает объекты.
  • Ключ собирает события одного заказа вместе, но не выстраивает их в том порядке, в котором всё произошло.
  1. Apache Flink 2.3: Keyed State.

© 2026 Grig Alex · CC BY-SA 4.0