← Назад к вопросам

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

1.8 Middle🔥 171 комментариев
#REST API и HTTP#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

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

Определение

Операция называется идемпотентной, если:

  • f(x) = f(f(x)) = f(f(f(x))) = ...
  • Неважно, сколько раз её выполнить — результат всегда одинаковый

Примеры идемпотентных операций

1. Математические операции

# Идемпотентная операция
def absolute_value(x):
    return abs(x)

print(absolute_value(-5))           # 5
print(absolute_value(absolute_value(-5)))  # 5
print(absolute_value(absolute_value(absolute_value(-5))))  # 5
# Результат всегда 5, независимо от количества применений

# Неидемпотентная операция
def increment(x):
    return x + 1

print(increment(5))              # 6
print(increment(increment(5)))   # 7
print(increment(increment(increment(5))))  # 8
# Результат разный при каждом применении

2. HTTP методы и REST API

# GET — идемпотентен (только читает данные)
GET /api/users/123  # Получит одного пользователя
GET /api/users/123  # Снова получит того же пользователя
GET /api/users/123  # И снова то же самое

# PUT — обычно идемпотентен (заменяет ресурс полностью)
PUT /api/users/123 { "name": "Иван" }  # Обновляет пользователя
PUT /api/users/123 { "name": "Иван" }  # Снова то же обновление
PUT /api/users/123 { "name": "Иван" }  # Результат всегда одинаковый

# POST — обычно НЕ идемпотентен (создаёт новый ресурс)
POST /api/users { "name": "Иван" }  # Создает пользователя 1
POST /api/users { "name": "Иван" }  # Создает пользователя 2
POST /api/users { "name": "Иван" }  # Создает пользователя 3
# Результат разный

# DELETE — идемпотентен
DELETE /api/users/123  # Удаляет пользователя
DELETE /api/users/123  # Пользователь уже удален (404)
DELETE /api/users/123  # Снова 404 — результат стабилен

3. Операции с базой данных

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker

# Идемпотентная операция — UPDATE
# Результат одинаков при многократном выполнении
SET user.name = 'Иван' WHERE user.id = 1;
SET user.name = 'Иван' WHERE user.id = 1;
SET user.name = 'Иван' WHERE user.id = 1;
# В базе у пользователя имя 'Иван' независимо от количества выполнений

# Неидемпотентная операция — INSERT
INSERT INTO users (name) VALUES ('Иван');
INSERT INTO users (name) VALUES ('Иван');
INSERT INTO users (name) VALUES ('Иван');
# Создадутся 3 разные записи

# Идемпотентная операция — DELETE
DELETE FROM users WHERE id = 1;
DELETE FROM users WHERE id = 1;  -- Не найдёт запись
DELETE FROM users WHERE id = 1;  -- Снова не найдёт
# Результат: пользователь удален (или не существует)

4. Операции в коде

# Идемпотентная функция
def set_user_status(user_id, status):
    """Установить статус пользователю. Можно вызвать несколько раз."""
    user = User.query.get(user_id)
    user.status = status  # ЗАМЕНЯЕМ значение
    db.session.commit()
    return user

set_user_status(1, 'active')  # user.status = 'active'
set_user_status(1, 'active')  # user.status = 'active' (то же самое)
set_user_status(1, 'active')  # user.status = 'active' (то же самое)

# Неидемпотентная функция
def increase_user_balance(user_id, amount):
    """Увеличить баланс пользователя. ОПАСНО вызывать несколько раз!"""
    user = User.query.get(user_id)
    user.balance += amount  # ПРИБАВЛЯЕМ к значению
    db.session.commit()
    return user

increase_user_balance(1, 100)  # balance = 100
increase_user_balance(1, 100)  # balance = 200
increase_user_balance(1, 100)  # balance = 300

5. Платежная система (критический пример)

# ❌ Неидемпотентный код (ОПАСНО!)
def process_payment(order_id, amount):
    """Обработать платеж. Повторный вызов создаст двойное списание!"""
    order = Order.query.get(order_id)
    account.balance -= amount  # Снимаем деньги
    transaction = Transaction(order_id=order_id, amount=amount)
    db.session.add(transaction)
    db.session.commit()
    return transaction

# Проблема: если сеть упадет и запрос повторится,
# деньги будут списаны дважды!

# ✅ Идемпотентный код (ПРАВИЛЬНО!)
def process_payment(order_id, amount, idempotency_key):
    """Обработать платеж. Безопасно вызывать несколько раз."""
    # Сначала проверяем, не обработали ли мы это платёж ранее
    existing = Transaction.query.filter_by(
        idempotency_key=idempotency_key
    ).first()
    
    if existing:
        # Платеж уже обработан, возвращаем старый результат
        return existing
    
    # Обрабатываем новый платеж
    account.balance -= amount
    transaction = Transaction(
        order_id=order_id,
        amount=amount,
        idempotency_key=idempotency_key
    )
    db.session.add(transaction)
    db.session.commit()
    return transaction

# Теперь безопасно:
process_payment(1, 100, 'key-123')  # Создает транзакцию
process_payment(1, 100, 'key-123')  # Возвращает ту же транзакцию
process_payment(1, 100, 'key-123')  # Возвращает ту же транзакцию

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

from fastapi import FastAPI, Header
from typing import Optional

app = FastAPI()

# API endpoint с поддержкой идемпотентности
@app.post("/api/v1/payments")
async def create_payment(
    amount: float,
    idempotency_key: str = Header(...),  # Обязательный заголовок
):
    # Проверяем, не обработали ли мы уже этот платёж
    cached = redis.get(f"payment:{idempotency_key}")
    if cached:
        return json.loads(cached)  # Возвращаем кэшированный результат
    
    # Обрабатываем новый платёж
    result = process_payment(amount)
    
    # Кэшируем результат на 24 часа
    redis.setex(
        f"payment:{idempotency_key}",
        86400,  # 24 часа
        json.dumps(result)
    )
    
    return result

Типы операций

# БЕЗОПАСНЫЕ И ИДЕМПОТЕНТНЫЕ
GET      # Только читает, никаких побочных эффектов
HEAD     # Как GET, но без тела
OPTIONS  # Запрос о возможностях
PUT      # Заменяет весь ресурс (идемпотентен)
DELETE   # Удаляет ресурс (идемпотентен)

# НЕБЕЗОПАСНЫЕ, ОБЫЧНО НЕ ИДЕМПОТЕНТНЫЕ
POST     # Создает новый ресурс (не идемпотентен)
PATCH    # Частичное обновление (обычно не идемпотентен)

Заключение

Идемпотентность — это фундаментальная концепция при проектировании надежных систем. Всякий раз, когда ты работаешь с операциями, которые могут быть повторены (платежи, отправка сообщений, обновления состояния), помни о необходимости идемпотентности. Используй idempotency keys, проверяй состояние перед выполнением операции и кэшируй результаты известных запросов. Это спасает от трудноуловимых bugs в распределенных системах.