惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
T
Tailwind CSS Blog
Microsoft Azure Blog
Microsoft Azure Blog
The Cloudflare Blog
博客园 - 叶小钗
N
Netflix TechBlog - Medium
罗磊的独立博客
量子位
MyScale Blog
MyScale Blog
A
About on SuperTechFans
Blog — PlanetScale
Blog — PlanetScale
V
Visual Studio Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
B
Blog
腾讯CDC
爱范儿
爱范儿
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
F
Fortinet All Blogs
雷峰网
雷峰网
G
Google Developers Blog
Google DeepMind News
Google DeepMind News

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Идемпотентность в System Design: полный пример
ph_piter (Из · 2026-05-13 · via Все публикации подряд на Хабре

Идемпотентность в System Design: полный пример

Идемпотентность часто упоминается при проектировании систем (system design). Ниже будет простыми словами объяснено, что это такое, далее мы разберём основные детали идемпотентности, часто понимаемые неверно и, наконец, проиллюстрируем её на полном примере. 

Что такое идемпотентность?

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

Иными словами, если вы выполняете идемпотентную операцию один раз, то можете быть уверены, что и на 2-й, и на 3-й, и на 10-й раз она завершится точно так же, как и на 1-й. Рассмотрим стандартный пример: у нас есть кнопки «вкл». и «выкл». Нажимая её, мы выполняем идемпотентную операцию. Нажмёте её один раз — машина включится. Если после этого нажимать её снова и снова, то ничего не изменится. Машина останется во включённом состоянии. То же самое касается кнопки «выкл».   

Рассмотрим пример из программирования:

def hide_my_button(self):
  self.show_my_button = False

Определённо, это идемпотентная операция.

def toggle_my_button_visibility(self):
  self.show_my_button = not self.show_my_button

А эта операция, конечно же, не идемпотентная.

Дело здесь совсем не в возвращаемом значении!

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

def hide_my_button(self):
  has_something_changed = self.show_my_button
  self.show_my_button = False
  return has_something_changed

Таким образом, функция возвращается независимо от того, изменилось что-нибудь или нет. Если вызывать эту функцию несколько раз, то возвращаемое ею значение может меняться! Но она всё равно является идемпотентной, так как идемпотентность характеризует воздействие на «состояние» или «эффект» и напрямую не связана с тем кодом состояния, который получит клиент.

Идемпотентность vs Чистота

Хотя, чистота функции не является темой этой статьи, я всё равно хочу коротко её затронуть, поскольку в этом заключается ещё одна распространённая путаница.

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

def square(my_number):
  return my_number ** 2

Это чистая функция. square(3) всегда будет одним и тем же числом.

def square_with_randomness(my_number):
  return (my_number ** 2) * random.uniform(0, 1)

Это не чистая функция. Функцияsquare_with_randomness(3)почти всегда будет давать разные результаты, поскольку мы умножаем её на случайное число в диапазоне от 0 до 1. Аналогично, если мы будем умножать её на какую-то глобальную переменную или переменную класса, она больше не будет чистой. Глобальная переменная может измениться, в таком случае изменится и результат.

Идемпотентная функция не обязана быть чистой. Рассмотрим следующий простой пример.

def save_name(name):
  my_database.save(name) # побочный эффект
  return name

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

Почему же эта концепция так важна при проектировании систем? На это есть много причин, и ниже мы обсудим основные из них.

Обработка сообщений

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

Проблема заключается в следующем. Когда  Service B потребляет сообщение, скажем, содержащее Event 3, он его обрабатывает, а затем записывает результат обработки в нашу базу данных. Оставим пример максимально простым и предположим, что Service B вычисляет для каждого события некую сложную формулу, и именно результат этого вычисления попадает в базу данных. Теперь важно обеспечить, чтобы ничего из этого ни в коем случае не потерялось.  Мы работаем с очень важными данными!

Но, если в процессе вычислений Service B откажет, либо если обнаружится фрагментация сети между Service B и базой данных, или если произойдёт что-то ещё, то и сообщение, и событие будут утрачены навсегда. Ужасно.

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

Но здесь возникает и другая проблема. Может случиться, что одно и то же сообщение будет прочитано дважды. Например, Service B выполнил вычисление и записал результат в базу данных, а затем что-то произошло. Из-за этого сервис аварийно завершил работу. Отказ сервиса произошёл в момент, когда сообщение ещё оставалось в очереди. Что произойдёт дальше? Сервис перезапустится и, возобновив работу, он продолжит потреблять сообщения. Причём, возобновит работу он именно с того сообщения, на котором остановился. Таким образом, это сообщение окажется обработано дважды!   

Как решается эта проблема? Напрямую — никак. Как известно, проектирование систем — это поле для компромиссов. Можно пойти либо на потерю сообщений, либо на двукратную обработку одних и тех же сообщений.

Но эта проблема не так серьёзна. Если мы спроектируем работу Service B как идемпотентную операцию, то ничего не случится. Сервис повторно потребит сообщение, но это не важно, поскольку результат будет точно таким, как и раньше.

Единственный недостаток заключается в некоторой дополнительной сложности (нужно найти способ, как сделать эту операцию идемпотентной) и в дополнительном расходе вычислительных ресурсов (потенциально нам потребуется делать что-то по несколько раз, а эти операции избыточны). Но, как правило, а в нашем случае — точно — это лучше, чем потерять сообщения!

Минимум однократная доставка + идемпотентность = строго однократная доставка

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

Подводные камни

Здесь есть несколько аспектов, с которыми нужно проявлять осторожность. Во-первых, может возникнуть бесконечный цикл (катастрофическое восстановление после отказов). Если у нас есть  “плохое” сообщение, например, записанное в нечитаемом формате, и именно из-за него отказывает сервис, потребляющий сообщения (Service B), то оно так и останется в очереди. Таким образом, Service B будет перезапускаться лишь для того, чтобы потребить то же самое плохое сообщение — и снова отказывать. И так снова и снова. Даже если в вашей системе пока нет таких проблем, рано или поздно они возникнут. Для устранения подобной проблемы используется так называемая «очередь недоставленных сообщений» (dead-letter queue), также называемая «госпиталь сообщений».

Как ещё можно использовать идемпотентность

Мы обсудили доставку сообщений, но идемпотентность встречается в проектировании систем повсюду. Давайте рассмотрим другие распространённые варианты её применения. 

API

Если вы пишете REST API, то уже имеете дело с идемпотентностью, даже если не осознаёте этого. В протоколе HTTP прямо определено, какие методы должны быть идемпотентными.  

Запросы GET ничего не меняют на сервере, поэтому, естественно, они идемпотентны. Их можно вызывать хоть 100 раз — результат всегда будет одинаков. Можно сколько угодно обновлять веб-страницу, не беспокоясь о том, что она может измениться.

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

Запросы DELETE предназначены для удаления ресурса. А если удалить что-то, что уже удалено? Его просто больше нет. Никаких проблем.

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

def create_user(request):
    idempotency_key = request.headers.get('Idempotency-Key')
    
    # Обрабатывали ли мы уже ранее именно этот запрос?
    if idempotency_key and already_processed(idempotency_key):
        return get_cached_response(idempotency_key)
    
    # Нет, создаём пользователя
    user = User.create(request.data)
    
    # Кэшируем отклик на будущее 
    if idempotency_key:
        cache_response(idempotency_key, user)
    
    return user

Базы данных

Операции над базами данных также зачастую бывают идемпотентными. Вот наиболее распространённые паттерны:

Операции UPSERT (INSERT или UPDATE при наличии) естественно идемпотентны. Можно 10 раз выполнить  upsert над одними и теми же данными – и всякий раз результат будет одинаков. Запись в базе данных либо создаётся один раз, либо сколько угодно раз обновляется одними и теми же значениями.

Распределённые системы

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

Полный пример: система обработки заказов

Хорошо, теперь давайте соберём всё разобранное здесь в конкретный пример, который соответствует системе, которая здесь схематически изображена. Имеется простой конвейер обработки заказов: заказы поступают из веб-приложения, проверяются сервисом заказа, поступают в очередь, а затем обслуживаются сервисом обработки заказов, который записывает информацию в базу данных.

Что мы создаём

Система устроена просто:

  1. Веб-приложение отправляет запросы HTTP POST, в которых содержатся данные о заказе

  2. Шлюз API обрабатывает маршрутизацпю, аутентификацию и ограничивает частоту передачи данных

  3. Сервис заказа проверяет заказы и публикует их в очереди

  4. Сообщения о заказах содержатся в Amazon SQS

  5. Сервис обработки заказов потребляет сообщения и записывает информацию в базу данных

  6. В базе данных с заказами содержится вся информация о наших заказах

  7. В очередь недоставленных сообщений отправляются все те сообщения, которые можно считать ядовитыми  

  8. Сервис уведомлений отправляет нашим клиентам подтверждения

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

Как сделать сервис обработки заказов идемпотентным

Сервис обработки заказов потребляет сообщения, поступающие от SQS, и выполняет конкретную бизнес-логику.

При обработке сообщений, то есть, когда происходит заказ, нам требуется:

  • Проверить, был ли уже обработан этот заказ

  • Если нет — вставить его в нашу OrdersDB

  • Если нет – приказать NotificationService, чтобы он отправил уведомление

Эта операция идемпотентна, поскольку мы проверяем, был ли уже обработан заказ. Для этого можно, например, выполнить запрос SELECT в OrderDB и вставлять данные лишь в случае, если их там пока нет. Схожую операцию можно выполнять для NotificationService или внутри NotificationService, работая с его собственной базой данных.

Правда, здесь будут возникать проблемы, связанные с конкурентностью, и с ними придётся справляться. Что, если у нас есть два разных экземпляра OrderProcessService, обрабатывающих одно и то же сообщение? И они оба одновременно выполняют SELECT. Тогда мы обработали бы сообщение дважды, а это нехорошо. Поэтому стоит обернуть данную логику в транзакцию.

В итоге у нас должно получиться нечто подобное:

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

У идемпотентности есть область действия

Этот момент также часто остаётся недопонятым: иногда идемпотентность понимают так, будто при многократном выполнении одной и той же идемпотентной операции совсем ничего не меняется. То есть, что последующие вызовы никак не подействуют на систему.

Это не совсем верно. Идемпотентность касается только операций бизнес-логики, а не состояния всей системы в целом.

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

  • Мы продолжаем логировать информацию о вызовах в журнале операций

  • Мы как и раньше записываем время отклика и собираем мониторинговые метрики  

  • Мы как и раньше валидируем запрос

  • Мы проверяем, обрабатывали ли мы этот заказ ранее

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