Идемпотентность: искусство не менять мир дважды

f9aaf4fbb970be34a56527d6ae72bfd6.gif

Привет! Я — Лера, и я — человек, который однажды понял, что прошлый опыт не помешает построить что-то новое. Эту статью я пишу для тех, кто хочет разобраться в сложных понятиях простыми словами.

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

038a827d2ab5ffc1df9c544427e42a60.png

Вы когда-нибудь многократно нажимали на кнопку вызова лифта, стоя у дверей, которые не закрываются быстрее от вашего нетерпения? На самом деле, лифт уже понял вас с первого раза. 

Это один из примеров идемпотентности — свойства, когда повторение одного и того же действия не изменяет результат. Забавно, но этот принцип не только сохраняет спокойствие лифтов, но и помогает во многих других сферах жизни и технологий. И кто знает, может знание об идемпотентности однажды избавит нас от лишних жестов и сэкономит наше время?

Идемпотентность — это когда вы делаете что-то снова и снова, но результат остаётся тем же, как если бы вы сделали это один раз. Представьте, что вы живете на 12 этаже, а лифт находится на первом. Вы нажимаете кнопку, чтобы вызвать его, и лифт приезжает к вам. Затем, даже если вы нажмете кнопку еще раз, лифт не отправится в новое путешествие вниз — он уже здесь, на вашем этаже. Это прекрасная демонстрация идемпотентности: лифт не реагирует на лишние нажатия кнопки.

Однако не все так просто в других аспектах жизни. Возьмем, к примеру, покупки в магазине. Если вы положите в корзину лимон, а затем добавите еще один, ваша корзина изменится: сначала в ней не было лимонов, а потом стало два. Этот процесс — неидемпотентный, ведь каждое действие изменяет состояние системы, или в данном случае — вашей корзины.

Давайте разберемся с идемпотентностью четырех основных HTTP-методов: GET, POST, PUT, и DELETE. На первый взгляд кажется, что идемпотентным является только метод GET, ведь он, казалось бы, не меняет состояние системы. Но не спешите с выводами — давайте подробнее рассмотрим каждый из них.

GET — этот метод как раз тот самый спокойный парень, который просто смотрит информацию, не вмешиваясь. Вы можете сколько угодно раз просить его показать одну и ту же страницу или данные, и состояние системы останется неизменным каждый раз. Например, помните момент в «Друзьях», когда Росс несколько раз подглядывал за Рейчел через глазок двери? Он мог смотреть бесконечно, но это никак не влияло на то, что происходило за дверью. Так и метод GET — он просто наблюдает, не вмешиваясь.

8781544e7f2b38ab63a71e82437d5350.gif

Теперь обратим внимание на метод PUT, который можно сравнить с переодеванием на вечеринке. Представим, что вы решили обновить свой образ, заменив футболку. Вы отправляете запрос в «гардеробную систему» вашего шкафа, выбирая новую футболку. Система обрабатывает запрос и «обновляет» ваш внешний вид.

Можно ли сказать, что этот метод меняет что-то каждый раз? На первый взгляд кажется, что да. Но давайте проверим ещё раз. Если вы отправите точно такой же запрос — снова выберите ту же футболку, — ваш внешний вид не изменится. Вы выглядите так же, как после первого «обновления». Вот и получается, что метод PUT идемпотентен: повторное применение одного и того же запроса не меняет результат.

Вспомним забавный момент из сериала «Друзья», когда Джоуи переоделся во все вещи Чендлера. Первый раз его внешний вид радикально изменился, но если бы он решил повторить этот трюк, результат остался бы прежним. Джоуи мог бы вновь надеть все те же вещи Чендлера, но внешне он выглядел бы так же, как после первой примерки. Это идеально иллюстрирует идемпотентность метода PUT — даже если действие повторить, изменения во «внешней системе» (в данном случае, в гардеробе) не произойдет.

c34f6ce1059a93ed167f6aeb652d1fa3.gif

Итак, вот интересная загадка: является ли метод DELETE идемпотентным? Подумайте секунду… Готовы разобраться?

Метод DELETE, это как большая красная кнопка «удалить» для данных на сервере. По сути, это способ сказать системе: «Этого больше не существует». Давайте посмотрим на практический пример.

Вспомните эпизод из сериала «Друзья», где Фиби, Джоуи и Моника пытались угадать, кто отец ребенка Рейчел. Моника как подсказку приносит свитер и кладет его на стол. Позже Росс заходит и забирает свой свитер. В этом случае Росс действует как метод DELETE: он убирает свитер, изменяя состояние «системы» (стола), где свитер больше не лежит. Теперь представим, что Росс входит снова, чтобы взять свитер, которого уже нет. Он обнаруживает пустой стол и уходит, не изменив ничего. Система осталась в том же состоянии, в каком была после первого удаления свитера.

a67da60d50670314e8c55398d293e4d8.gif

Так что да, метод DELETE идемпотентен. Он делает свою работу один раз, и повторные попытки ничего не изменят — свитер уже удален, стол пуст.

Итак, перейдем к самому интересному — методу POST. Может показаться, что если такие методы, как PUT и DELETE, идемпотентны, то POST точно такой же, правда? Но давайте разберемся поподробнее.

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

7a899cd96a4ef30f24e9895381ead125.gif

Так что нет, метод POST не идемпотентен. Он всегда добавляет что-то новое или меняет систему, точно как каждый глоток виски в бокале Тони Старка.

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

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

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

Своего рода токены идемпотентности — защитники от хаоса в запросах. Представьте, что вы отправляете запрос на оплату. Вместе с данными системы прикладывают к нему специальный уникальный ключ, например, idempotency_key: «abc123». Этот ключ — как паспорт, который позволяет системе понять, что это за операция и была ли она уже выполнена.

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

Лично у меня токены идемпотентности ассоциируются со сценой из Гарри Поттера, где школа отправляла Гарри письма о зачислении, где каждое из них помечалось как уникальное. Если письмо не доходило (спасибо Дурсли!), сова просто присылала новое, но только до тех пор, пока школа не получила ответа. Как только Гарри наконец согласился, повторные письма больше не отправлялись: система зафиксировала результат, и совы выдохнули.

Request: Первый запрос
{
  "idempotency_key": "abc123",
  "operation": "payment",
  "amount": 100,
  "currency": "USD",
  "payment_method": "credit_card",
  "customer_id": "cust_001"
}
Response: Первый запрос
{
  "status": "success",
  "transaction_id": "txn_001",
  "message": "Payment processed successfully.",
  "amount": 100,
  "currency": "USD"
}
Первый запрос с idempotency_key: "abc123" обрабатывается нормально, создается транзакция txn_001.
Request: Повторный запрос с тем же токеном
{
  "idempotency_key": "abc123",
  "operation": "payment",
  "amount": 100,
  "currency": "USD",
  "payment_method": "credit_card",
  "customer_id": "cust_001"
}
Response: Повторный запрос
{
  "status": "duplicate",
  "transaction_id": "txn_001",
  "message": "Duplicate request detected. Original transaction details returned.",
  "amount": 100,
  "currency": "USD"
}
Повторный запрос с тем же idempotency_key: "abc123" возвращает данные уже существующей транзакции (txn_001) без повторной обработки.

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

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

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

 Request: Первый запрос
{
  "request_id": "req_001",
  "operation": "book_taxi",
  "pickup_location": "ул. Ленина, д. 10",
  "dropoff_location": "пр. Мира, д. 25",
  "customer_id": "cust_001",
  "timestamp": "2024-11-24T10:00:00Z"
}
Response: Первый запрос
{
  "status": "success",
  "booking_id": "booking_001",
  "message": "Taxi successfully booked.",
  "pickup_time": "2024-11-24T10:15:00Z"
}
Request: Повторный запрос в пределах временного окна (1 минута спустя)
{
  "request_id": "req_002",
  "operation": "book_taxi",
  "pickup_location": "ул. Ленина, д. 10",
  "dropoff_location": "пр. Мира, д. 25",
  "customer_id": "cust_001",
  "timestamp": "2024-11-24T10:01:00Z"
}
Response: Повторный запрос в пределах временного окна
{
  "status": "duplicate",
  "booking_id": "booking_001",
  "message": "Duplicate request detected. Returning details of the existing booking.",
  "pickup_time": "2024-11-24T10:15:00Z"
}
Request: Новый запрос за пределами временного окна (5 минут спустя)
{
  "request_id": "req_003",
  "operation": "book_taxi",
  "pickup_location": "ул. Ленина, д. 10",
  "dropoff_location": "пр. Мира, д. 25",
  "customer_id": "cust_001",
  "timestamp": "2024-11-24T10:05:00Z"
}
Response: Новый запрос за пределами временного окна
{
  "status": "success",
  "booking_id": "booking_002",
  "message": "Taxi successfully booked.",
  "pickup_time": "2024-11-24T10:20:00Z"
}

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

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

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

# Максимальное количество людей на кухне одновременно

MAX_CAPACITY = 2

# Семафор, ограничивающий количество одновременных операций (людей на кухне)

semaphore = threading.Semaphore (MAX_CAPACITY)

# Функция для имитации работы на кухне

def work_on_kitchen (person_id):

    print (f«Человек {person_id} вошел на кухню.»)

    # Захватываем семафор, чтобы начать работу

    semaphore.acquire ()

    # Имитация работы на кухне

    print (f«Человек {person_id} работает на кухне.»)

    time.sleep (2)  # Имитируем время работы (например, 2 секунды)

    # Освобождаем семафор, чтобы другие могли работать

    print (f«Человек {person_id} покинул кухню.»)

    semaphore.release ()

# Запускаем несколько потоков, имитируя людей, желающих работать на кухне

threads = []

for i in range (5):   # 5 человек пытаются попасть на кухню

    t = threading.Thread (target=work_on_kitchen, args=(i+1,))

    threads.append (t)

    t.start ()

# Ожидаем завершения всех потоков

for t in threads:

    t.join ()

print («Все люди завершили работу на кухне.»)

Инициализация семафора: Мы создаем объект семафора semaphore с лимитом в 2, что означает, что одновременно могут работать не более двух человек (или выполнять операции).

Функция work_on_kitchen: Каждый «человек» (поток) пытается зайти на кухню. Если на кухне уже два человека, остальные будут ждать.

Метод acquire: При заходе на кухню каждый поток «захватывает» семафор, который ограничивает количество потоков (людей).

Метод release: После выполнения работы на кухне человек освобождает место, позволяя следующему потоку войти.

Запуск потоков: Мы создаем и запускаем 5 потоков, представляющих людей, которые хотят попасть на кухню.

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

Вот такой вывод будет у программы:

Человек 1 вошел на кухню.

Человек 2 вошел на кухню.

Человек 1 работает на кухне.

Человек 2 работает на кухне.

Человек 1 покинул кухню.

Человек 3 вошел на кухню.

Человек 3 работает на кухне.

Человек 2 покинул кухню.

Человек 4 вошел на кухню.

Человек 4 работает на кухне.

Человек 3 покинул кухню.

Человек 5 вошел на кухню.

Человек 5 работает на кухне.

Человек 4 покинул кухню.

Человек 5 покинул кухню.

Все люди завершили работу на кухне.

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

Итак, вот он мир идемпотентности — от лифтов до финансовых транзакций. Этот принцип не просто технический термин; он влияет на то, как мы взаимодействуем с технологиями каждый день. Понимание идемпотентности помогает нам видеть, почему иногда меньше действий приводит к лучшим результатам и как мы можем управлять повторными действиями, чтобы не вызывать хаос.

Мы также узнали, что даже неидемпотентные методы можно «укротить» искусственно, что делает наши системы более устойчивыми и надежными. Надеемся, что теперь, когда вы нажимаете кнопку лифта или отправляете очередной онлайн-платеж, вы будете чувствовать себя немного больше в курсе того, что происходит за кулисами цифрового мира.

В конце концов, знание — это не только сила, но и способ сделать нашу жизнь проще и удобнее. 

Рада, что Вы прочитали статью до самого конца! Вопросы, комментарии и идеи — с радостью жду их ниже.

© Habrahabr.ru