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

Какие знаешь идемпотентные методы?

1.3 Junior🔥 111 комментариев

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

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

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

Идемпотентные методы в REST и HTTP

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

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

# Идемпотентная операция
# f(f(x)) = f(x)

def make_idempotent():
    """Операция можно повторять без побочных эффектов"""
    pass

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

# Математика: abs(abs(-5)) = abs(-5) = 5
print(abs(abs(-5)))  # 5
print(abs(-5))       # 5 — одинаковый результат

# Множества: {1,2,3} ∪ {1,2,3} = {1,2,3}
a = {1, 2, 3}
b = a | a
print(b)  # {1, 2, 3} — не изменилось

# Примеры НЕ идемпотентных операций:
count = 0
def increment():
    global count
    count += 1
    return count

print(increment())  # 1
print(increment())  # 2 — разные результаты!

2. HTTP методы и идемпотентность

Метод  | Идемпотент | Безопасен | Примечание
================================================
GET    | ✅ Да      | ✅ Да     | Только чтение
HEAD   | ✅ Да      | ✅ Да     | GET без тела
OPTIONS| ✅ Да      | ✅ Да     | Информация о методах
POST   | ❌ Нет     | ❌ Нет    | Создание — не идемпотент
PUT    | ✅ Да      | ❌ Нет    | Полное обновление — идемпотент
PATCH  | ❓ Может   | ❌ Нет    | Зависит от реализации
DELETE | ✅ Да      | ❌ Нет    | Удаление дважды — OK
TRACE  | ✅ Да      | ✅ Да     | Отладка

3. GET — идемпотентен

# GET должен быть полностью идемпотентным
from fastapi import FastAPI

app = FastAPI()

users_db = {
    1: {"id": 1, "name": "Alice"},
    2: {"id": 2, "name": "Bob"},
}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    """GET идемпотентен — результат не меняется"""
    return users_db.get(user_id)

# GET /users/1 → {"id": 1, "name": "Alice"}
# GET /users/1 → {"id": 1, "name": "Alice"} (одинаково)
# GET /users/1 → {"id": 1, "name": "Alice"} (одинаково)

# ✅ Правильно — GET не имеет побочных эффектов

4. PUT — идемпотентен

# PUT полностью заменяет ресурс

@app.put("/users/{user_id}")
async def update_user(user_id: int, data: dict):
    """PUT идемпотентен — результат одинаковый"""
    users_db[user_id] = {"id": user_id, **data}
    return users_db[user_id]

# Первый запрос: PUT /users/1 {"name": "Alice"}
# users_db[1] = {"id": 1, "name": "Alice"}

# Второй запрос: PUT /users/1 {"name": "Alice"}
# users_db[1] = {"id": 1, "name": "Alice"} (одинаково)

# Третий запрос: PUT /users/1 {"name": "Alice"}
# users_db[1] = {"id": 1, "name": "Alice"} (одинаково)

# ✅ Идемпотентен — результат не меняется

5. DELETE — идемпотентен

@app.delete("/users/{user_id}")
async def delete_user(user_id: int):
    """DELETE идемпотентен — повторное удаление OK"""
    if user_id in users_db:
        del users_db[user_id]
        return {"status": "deleted"}
    return {"status": "not found"}  # Второй раз — не найден

# Первый запрос: DELETE /users/1
# users_db удаляется → {"status": "deleted"}

# Второй запрос: DELETE /users/1
# users_db пуст → {"status": "not found"}

# ❓ Это идемпотентно? ДА!
# Почему? Потому что результат стабилен:
# - Изменений в состоянии больше нет
# - Статус код правильный (404 вместо 200)

6. PATCH — НЕ идемпотентен (обычно)

# PATCH частично обновляет

@app.patch("/users/{user_id}")
async def patch_user(user_id: int, data: dict):
    """❌ PATCH может быть НЕ идемпотентным"""
    # Неправильно: увеличиваем счётчик
    if "score" in data:
        users_db[user_id]["score"] = users_db[user_id].get("score", 0) + data["score"]
    return users_db[user_id]

# Первый запрос: PATCH /users/1 {"score": 10}
# users_db[1]["score"] = 0 + 10 = 10

# Второй запрос: PATCH /users/1 {"score": 10}
# users_db[1]["score"] = 10 + 10 = 20 ← ДРУГОЙ результат!

# ✅ Правильно: заменяем поле
@app.patch("/users/{user_id}")
async def patch_user_correct(user_id: int, data: dict):
    """✅ PATCH идемпотентен если просто заменяем"""
    users_db[user_id].update(data)
    return users_db[user_id]

# Первый запрос: PATCH /users/1 {"name": "Alice"}
# users_db[1]["name"] = "Alice"

# Второй запрос: PATCH /users/1 {"name": "Alice"}
# users_db[1]["name"] = "Alice" (одинаково)

7. POST — НЕ идемпотентен

@app.post("/users")
async def create_user(data: dict):
    """❌ POST НЕ идемпотентен — создаёт новый ресурс"""
    user_id = max(users_db.keys()) + 1
    users_db[user_id] = {"id": user_id, **data}
    return users_db[user_id]

# Первый запрос: POST /users {"name": "Alice"}
# Создан пользователь с ID 3

# Второй запрос: POST /users {"name": "Alice"}
# Создан пользователь с ID 4 ← НОВЫЙ ID!

# ❌ НЕ идемпотентен

8. Идемпотентные ключи (Idempotency Key)

# Для POST-запросов можно добавить Idempotency-Key
from fastapi import Header, HTTPException
from uuid import UUID
import hashlib

processed_requests = {}  # {idempotency_key: response}

@app.post("/payments")
async def process_payment(
    data: dict,
    idempotency_key: str = Header(...)
):
    """Идемпотентный POST с ключом"""
    # Проверить, был ли уже обработан
    if idempotency_key in processed_requests:
        return processed_requests[idempotency_key]
    
    # Обработать платёж
    result = {
        "transaction_id": f"tx_{idempotency_key[:8]}",
        "amount": data["amount"],
        "status": "completed"
    }
    
    # Сохранить результат
    processed_requests[idempotency_key] = result
    return result

# Клиент генерирует UUID для каждого платежа
# Если сеть разорвалась, можно отправить ещё раз с ТАКИМ ЖЕ ключом
# Сервер вернёт ТОТ ЖЕ результат

9. Реализация идемпотентности в базе данных

from sqlalchemy import Column, String, Integer, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session

Base = declarative_base()

class Payment(Base):
    __tablename__ = "payments"
    
    id = Column(Integer, primary_key=True)
    idempotency_key = Column(String, unique=True, nullable=False)
    user_id = Column(Integer, nullable=False)
    amount = Column(Integer, nullable=False)
    status = Column(String, default="pending")

def process_payment_idempotent(db: Session, idempotency_key: str, user_id: int, amount: int):
    """Обработать платёж идемпотентно"""
    # 1. Проверить, существует ли уже такой платёж
    existing = db.query(Payment).filter_by(
        idempotency_key=idempotency_key
    ).first()
    
    if existing:
        return existing  # Вернуть существующий
    
    # 2. Создать новый
    payment = Payment(
        idempotency_key=idempotency_key,
        user_id=user_id,
        amount=amount,
        status="completed"
    )
    db.add(payment)
    db.commit()
    return payment

# Несколько попыток с одним ключом → один платёж

10. Приемные правила (Exactly-Once Semantics)

# При обработке асинхронных задач
from celery import Celery
from redis import Redis

redis = Redis()
app = Celery()

@app.task
def idempotent_task(task_id: str, data: dict):
    """Идемпотентная асинхронная задача"""
    # 1. Проверить, обработана ли уже
    if redis.exists(f"task:{task_id}:processed"):
        return redis.get(f"task:{task_id}:result")
    
    # 2. Обработать
    result = process_data(data)
    
    # 3. Сохранить результат (atomic)
    redis.set(f"task:{task_id}:processed", "1", ex=3600)  # 1 час
    redis.set(f"task:{task_id}:result", result, ex=3600)
    
    return result

11. Когда нужна идемпотентность?

# ✅ Обязательно идемпотентно:
# - Платежи
# - Заказы
# - Операции со счётами
# - Критические операции

# ❌ Не критично:
# - Логирование просмотров
# - Аналитика
# - Некритичные операции

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

Итоги

Идемпотентные методы:

  • GET, HEAD, OPTIONS — всегда
  • PUT — обычно (если правильно реализовано)
  • DELETE — да (повторное удаление OK)
  • PATCH — только если заменяем, не трансформируем
  • POST — НЕ идемпотентен по умолчанию

Для идемпотентности POST:

  • Используй Idempotency-Key заголовок
  • Сохраняй результаты обработанных ключей
  • Проверяй существование перед созданием
  • Используй уникальные constraint'ы в БД

Правило: если операция может быть отправлена повторно (из-за timeout, retry, ошибки сети), она ДОЛЖНА быть идемпотентной