Комментарии (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, ошибки сети), она ДОЛЖНА быть идемпотентной