Какие принципы построения идемпотентных сервисов знаете?csharp-67

Определение идемпотентности

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

Ключевые принципы построения

1. Использование уникальных идентификаторов запросов

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);
}

2. Проверка состояния перед выполнением

public async Task UpdateOrderStatus(Guid orderId, OrderStatus status)
{
    var currentStatus = await _repository.GetStatusAsync(orderId);

    if (currentStatus == status)
        return; // Уже в нужном состоянии

    await _repository.UpdateStatusAsync(orderId, status);
}

3. Реализация операций UPSERT

public async Task UpsertProduct(Product product)
{
    await _db.Products
        .Upsert(product)
        .On(p => p.Id)
        .RunAsync();
}

4. Использование ETag для оптимистичной блокировки

[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);
}

5. Паттерн "Компенсирующая транзакция"

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;
    }
}

6. Дедупликация сообщений в обработчиках событий

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));
    }
}

Технические реализации в .NET

1. Атрибут идемпотентности

[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();
    }
}

2. Идемпотентные HTTP-методы

МетодИдемпотентен?Обоснование
GETДаТолько получение данных
PUTДаПолная замена ресурса
DELETEДаПовторное удаление не меняет состояние
POSTНетСоздает новый ресурс
PATCHЗависитЧастичное обновление может быть неидемпотентным

Хранение состояния идемпотентности

  1. Распределенный кэш (Redis):

    • Временное хранение ключей
    • Высокая производительность
  2. База данных:

    • Таблица обработанных запросов
    • TTL для автоматической очистки
  3. Гибридный подход:

    • Горячие данные в кэше
    • Холодные данные в БД

Ошибки проектирования

  1. Использование временных меток вместо ID:

    • Могут коллизировать
    • Нет гарантии уникальности
  2. Слишком короткое TTL для ключей:

    • Возможны дубликаты при длительных задержках
  3. Игнорирование порядка запросов:

    • Старый запрос может перезаписать новый

Резюмируем:

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