Идемпотентность — свойство операции, при котором повторное её выполнение дает тот же результат, что и первое выполнение. Для веб-сервисов это означает, что клиент может безопасно повторять запросы при таймаутах или сбоях.
public async Task<ActionResult> ProcessPayment(
[FromBody] PaymentRequest request,
[FromHeader(Name = "Idempotency-Key")] string idempotencyKey)
{
if (await _cache.ExistsAsync(idempotencyKey))
{
var cachedResponse = await _cache.GetAsync<PaymentResponse>(idempotencyKey);
return Ok(cachedResponse);
}
var response = await _paymentService.Process(request);
await _cache.SetAsync(idempotencyKey, response, TimeSpan.FromHours(24));
return Ok(response);
}
public async Task UpdateOrderStatus(Guid orderId, OrderStatus status)
{
var currentStatus = await _repository.GetStatusAsync(orderId);
if (currentStatus == status)
return; // Уже в нужном состоянии
await _repository.UpdateStatusAsync(orderId, status);
}
public async Task UpsertProduct(Product product)
{
await _db.Products
.Upsert(product)
.On(p => p.Id)
.RunAsync();
}
[HttpPut("products/{id}")]
public async Task<IActionResult> UpdateProduct(
Guid id,
[FromBody] ProductUpdate update,
[FromHeader(Name = "If-Match")] string etag)
{
var current = await _repository.GetAsync(id);
if (current.ETag != etag)
return Conflict("Объект был изменен другим запросом");
var updated = await _repository.UpdateAsync(id, update);
return Ok(updated);
}
public async Task CancelOrder(Guid orderId)
{
var order = await _repository.GetAsync(orderId);
if (order.Status == OrderStatus.Cancelled)
return;
try
{
await _paymentService.Refund(order.PaymentId);
await _inventoryService.Restock(order.Items);
await _repository.UpdateStatus(orderId, OrderStatus.Cancelled);
}
catch
{
// Компенсирующие действия идемпотентны по своей природе
await _paymentService.RevertRefund(order.PaymentId);
throw;
}
}
public class OrderCreatedHandler : IEventHandler<OrderCreatedEvent>
{
public async Task Handle(OrderCreatedEvent @event)
{
if (await _processedMessages.ContainsAsync(@event.MessageId))
return;
// Логика обработки
await _processedMessages.AddAsync(@event.MessageId, TimeSpan.FromDays(7));
}
}
[AttributeUsage(AttributeTargets.Method)]
public class IdempotentAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var idempotencyKey = context.HttpContext.Request.Headers["Idempotency-Key"];
if (string.IsNullOrEmpty(idempotencyKey))
{
context.Result = new BadRequestResult();
return;
}
if (await IsDuplicateRequest(idempotencyKey))
{
context.Result = new ConflictResult();
return;
}
await next();
}
}
Метод | Идемпотентен? | Обоснование |
---|---|---|
GET | Да | Только получение данных |
PUT | Да | Полная замена ресурса |
DELETE | Да | Повторное удаление не меняет состояние |
POST | Нет | Создает новый ресурс |
PATCH | Зависит | Частичное обновление может быть неидемпотентным |
Распределенный кэш (Redis):
База данных:
Гибридный подход:
Использование временных меток вместо ID:
Слишком короткое TTL для ключей:
Игнорирование порядка запросов:
построение идемпотентных сервисов требует продуманного подхода к проектированию API, обработке запросов и хранению состояния. Ключевые принципы включают использование уникальных идентификаторов, проверку текущего состояния перед изменениями и реализацию компенсирующих операций. В распределенных системах идемпотентность становится критически важным свойством для обеспечения надежности.