Перейти к содержимому

CAP, идемпотентность и модели согласованности

Зачем знать: Распределённые системы живут в мире компромиссов. CAP-теорема, PACELC, модели consistency и идемпотентность — это фундамент, на котором строятся платежи, очереди, репликация и любая интеграция между сервисами. Middle Go-разработчик должен уметь объяснить, почему банковская транзакция использует CP, а лента новостей — AP, и как написать идемпотентный HTTP-handler, который выдерживает шторм retry от webhook-провайдера.

  1. Концепция: CAP, PACELC, модели consistency
  2. Глубже: идемпотентность, реализация Idempotency-Key, BASE vs ACID
  3. Gotchas
  4. Real cases
  5. Вопросы (25)
  6. Practice
  7. Источники

Eric Brewer в 2000 году на PODC keynote сформулировал гипотезу, формально доказанную Gilbert и Lynch в 2002. Теорема говорит: в распределённой системе из трёх свойств — Consistency, Availability, Partition tolerance — при возникновении сетевого разделения (partition) можно сохранить только два.

Consistency
/\
/ \
/ \
/ \
/ CP \
/ \
/ CA (миф) \
/ \
/________________\
Partition Availability
tolerance

Определения:

  • Consistency (C): все ноды видят одинаковое состояние данных в одно время. Каждый read возвращает либо последний write, либо ошибку.
  • Availability (A): каждый запрос получает ответ (success или error), без гарантии актуальности. Никаких таймаутов или зависаний.
  • Partition tolerance (P): система продолжает работать при потере сообщений между узлами (network partition).

Practical insight: P неизбежен в реальной распределённой системе. Сеть рвётся, switch’и падают, rack’и теряют power. Поэтому выбор реально стоит между CP и AP во время partition.

Примеры:

  • CP: PostgreSQL с синхронной репликацией, etcd, ZooKeeper, MongoDB (в strong-consistency режиме), HBase. При partition — ноды на minority side не отвечают на writes.
  • AP: Cassandra (с tunable consistency), DynamoDB (eventually consistent reads), Riak, CouchDB. При partition — все ноды отвечают, но могут давать stale data.

⚠️ CA — это миф. Система без partition tolerance может работать только на единственной машине. В распределённой среде partition неизбежен.

CAP описывает только поведение при partition. Но 99% времени partition нет — что тогда? Abadi расширил CAP:

If Partition (P) — choose between Availability (A) and Consistency (C); Else (E) — choose between Latency (L) and Consistency (C).

PACELC классификация:
PA/EL (max availability, min latency): Cassandra, Riak, Dynamo
PA/EC (avail при partition, consist иначе): MongoDB (default)
PC/EL (consist при partition, low latency иначе): редкий случай
PC/EC (strong consistency всегда): HBase, BigTable, VoltDB

Trade-off в нормальной работе: хочешь strong consistency — плати latency (синхронная репликация на N реплик). Хочешь low latency — мирись с eventual consistency (асинхронная репликация, чтение с любой реплики).

От самой строгой к самой слабой:

Каждый read видит результат последнего write мгновенно. Невозможно в распределённой системе (нарушает теорию относительности: нет понятия “одновременно” при разнесённых узлах).

“Single-copy semantics”. Операции кажутся атомарными и упорядоченными по real-time. Если op B стартовала после завершения op A, B видит результат A.

Это самая сильная достижимая модель. Реализована: etcd, ZooKeeper, Spanner (через TrueTime), Aurora с raft consensus.

⚠️ Linearizability ≠ serializability. Linearizability — про read/write одного объекта. Serializability — про транзакции (multi-object). Strict serializability = serializability + linearizability.

Операции выглядят упорядоченными в некотором глобальном порядке, и порядок каждого процесса сохраняется. Но real-time порядок может нарушаться.

Слабее linearizability. Достаточно для CPU memory models, не подходит для финансов.

Операции, связанные причинной зависимостью (happens-before, см. Lamport timestamps), упорядочены. Независимые — могут быть в любом порядке.

Пример: если ты лайкнул пост, потом написал коммент “круто!”, другой пользователь не должен увидеть коммент раньше лайка. Но порядок twoindependent комментариев не важен.

Реализация: vector clocks, version vectors. Используется в COPS, Riak.

Если writes прекратятся, в конце концов все реплики сойдутся к одному значению. Никаких гарантий о том, когда и в каком порядке.

DNS, S3 (раньше; теперь strong read-after-write для PUT), Cassandra, Dynamo.

Convergence механизмы:

  • Last-Write-Wins (LWW): конфликты решаются по timestamp. Опасно при clock skew.
  • CRDT (Conflict-free Replicated Data Types): структуры с математически коммутативными операциями. G-Counter, OR-Set, LWW-Register. Используются в Redis (HyperLogLog), Riak, Automerge.
  • Vector clocks: обнаружение concurrent writes, разрешение на уровне приложения.
  • Read-your-writes (RYW): клиент после своего write видит свой write при последующем read. Обычно реализуется через sticky session или client-side caching.
  • Monotonic reads: последующие reads клиента не возвращают более старые версии.
  • Monotonic writes: writes одного клиента применяются в порядке отправки.
  • Writes-follow-reads: write после read видит то, что было прочитано.

“Read данных не старше T секунд / N версий”. Компромисс: latency низкий, но stale data ограничен.

Поддерживается в Azure Cosmos DB как уровень consistency. Также в Spanner (через snapshot reads).

ACID (traditional RDBMS):

  • Atomicity — всё или ничего
  • Consistency — переход между валидными состояниями (constraints, FK)
  • Isolation — конкурентные транзакции изолированы
  • Durability — committed данные не теряются

BASE (NoSQL philosophy, Brewer):

  • Basically Available — система всегда отвечает (возможно, stale)
  • Soft state — состояние меняется без input (background convergence)
  • Eventual consistency — сходимость со временем

ACID и BASE — не взаимоисключающие. Современные системы (Spanner, CockroachDB, FoundationDB) показали, что ACID масштабируется. Но цена — latency и сложность реализации.

Когда eventual consistency работает:

  • Counters лайков, просмотров (точное число не важно)
  • Activity feed (порядок может слегка плыть)
  • Поисковые индексы (Elasticsearch async pipeline)
  • Кэши (TTL invalidation)
  • DNS, CDN

Когда eventual consistency НЕ работает:

  • Деньги: транзакции, балансы, переводы
  • Inventory: бронирование последнего товара
  • Authentication: блокировка скомпрометированного токена
  • Locking: распределённые блокировки

⚠️ Gotcha: eventual consistency требует от приложения умения работать со stale data. UI должен показывать loading, retries должны быть идемпотентными, конфликты — резолвиться.


Идемпотентность (от лат. idem potens — “то же самое могущественное”): операция f идемпотентна, если f(f(x)) = f(x) для любого x.

В распределённых системах: операция идемпотентна, если её повторное выполнение приводит к тому же результату.

Зачем нужна:

  • Сети ненадёжны. TCP retry, прокси retry, клиентский retry.
  • At-least-once delivery в message queues (Kafka, RabbitMQ).
  • Webhook providers (Stripe, GitHub) ретраят при ошибках.
  • Mobile клиенты дублируют запросы при flaky network.

Без идемпотентности: двойной retry создаёт двойной заказ, двойной перевод, двойной email.

МетодИдемпотентенSafe
GETДаДа
HEADДаДа
OPTIONSДаДа
PUTДаНет
DELETEДаНет
POSTНетНет
PATCHНетНет

Safe — операция не меняет состояние (read-only). Идемпотентность — повтор операции даёт тот же эффект.

Почему POST не идемпотентен: POST /orders создаёт новый ресурс каждый раз. Чтобы сделать его идемпотентным — нужен Idempotency-Key.

PUT vs POST:

  • PUT /users/123 — заменить ресурс с id=123. Идемпотентно: повтор даст то же состояние.
  • POST /users — создать. Неидемпотентно: повтор создаст дубль.

Клиент при отправке POST включает заголовок Idempotency-Key: <unique-id>. Сервер сохраняет результат для key и возвращает кэшированный ответ при повторе.

Алгоритм:

  1. Клиент генерирует UUID (на retry — тот же ключ).
  2. Сервер при получении POST:
    • Проверяет таблицу idempotency_keys — есть ли запись с этим key?
    • Если есть и status=completed — возвращает saved response.
    • Если есть и status=in_progress — возвращает 409 или ждёт.
    • Если нет — создаёт запись со status=in_progress, выполняет операцию, сохраняет result.
  3. TTL для cleanup (24-48 часов обычно).

Схема таблицы:

CREATE TABLE idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
request_hash VARCHAR(64) NOT NULL, -- SHA256 от body, для detection mismatch
status VARCHAR(20) NOT NULL, -- in_progress | completed | failed
response_status INT,
response_body JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL -- TTL для cleanup
);
CREATE INDEX idx_idempotency_expires ON idempotency_keys(expires_at);
package idempotency
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
type Store struct {
db *pgxpool.Pool
}
type Record struct {
Key string
RequestHash string
Status string
ResponseStatus int
ResponseBody []byte
}
func (s *Store) Get(ctx context.Context, key string) (*Record, error) {
row := s.db.QueryRow(ctx,
`SELECT key, request_hash, status, response_status, response_body
FROM idempotency_keys WHERE key=$1 AND expires_at > NOW()`, key)
r := &Record{}
err := row.Scan(&r.Key, &r.RequestHash, &r.Status, &r.ResponseStatus, &r.ResponseBody)
if err != nil {
return nil, err
}
return r, nil
}
func (s *Store) StartOrLoad(ctx context.Context, key, hash string) (*Record, bool, error) {
// Атомарно: INSERT, иначе вернуть существующую
var r Record
err := s.db.QueryRow(ctx, `
INSERT INTO idempotency_keys (key, request_hash, status, expires_at)
VALUES ($1, $2, 'in_progress', NOW() + INTERVAL '24 hours')
ON CONFLICT (key) DO UPDATE SET key = idempotency_keys.key -- noop
RETURNING key, request_hash, status, response_status, response_body
`, key, hash).Scan(&r.Key, &r.RequestHash, &r.Status, &r.ResponseStatus, &r.ResponseBody)
if err != nil {
return nil, false, err
}
created := r.Status == "in_progress" && r.RequestHash == hash
return &r, created, nil
}
func (s *Store) Complete(ctx context.Context, key string, status int, body []byte) error {
_, err := s.db.Exec(ctx, `
UPDATE idempotency_keys
SET status='completed', response_status=$2, response_body=$3, completed_at=NOW()
WHERE key=$1
`, key, status, body)
return err
}
// Middleware
func Middleware(store *Store) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" || r.Method == http.MethodGet {
next.ServeHTTP(w, r)
return
}
// Hash body для detection mismatch
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
h := sha256.Sum256(body)
hash := hex.EncodeToString(h[:])
rec, created, err := store.StartOrLoad(r.Context(), key, hash)
if err != nil {
http.Error(w, "internal", 500)
return
}
// Mismatch: same key, different body — конфликт
if rec.RequestHash != hash {
http.Error(w, "idempotency key reuse with different body", 422)
return
}
// Кэшированный ответ
if rec.Status == "completed" {
w.WriteHeader(rec.ResponseStatus)
w.Write(rec.ResponseBody)
return
}
if !created && rec.Status == "in_progress" {
http.Error(w, "request in progress", 409)
return
}
// Захват response
rw := &responseRecorder{ResponseWriter: w, status: 200, body: &bytes.Buffer{}}
next.ServeHTTP(rw, r)
store.Complete(r.Context(), key, rw.status, rw.body.Bytes())
})
}
}
type responseRecorder struct {
http.ResponseWriter
status int
body *bytes.Buffer
}
func (r *responseRecorder) WriteHeader(s int) {
r.status = s
r.ResponseWriter.WriteHeader(s)
}
func (r *responseRecorder) Write(b []byte) (int, error) {
r.body.Write(b)
return r.ResponseWriter.Write(b)
}

2.5 Lock per key (предотвращение race на параллельных запросах)

Заголовок раздела «2.5 Lock per key (предотвращение race на параллельных запросах)»

Если два параллельных request с одним Idempotency-Key пришли одновременно — нужна сериализация:

Через advisory lock PostgreSQL:

hashID := int64(crc32.ChecksumIEEE([]byte(key)))
_, err := db.Exec(ctx, "SELECT pg_advisory_xact_lock($1)", hashID)
// в той же транзакции — работа с idempotency_keys

Через Redis SETNX:

ok, _ := rdb.SetNX(ctx, "idem:lock:"+key, "1", 30*time.Second).Result()
if !ok {
return errors.New("conflict")
}
defer rdb.Del(ctx, "idem:lock:"+key)

Database level:

  • INSERT ... ON CONFLICT DO NOTHING (PostgreSQL upsert).
  • Unique constraint на бизнес-ключ.
  • Условные updates: UPDATE accounts SET balance=balance-100 WHERE id=$1 AND tx_id NOT IN (SELECT id FROM processed_tx WHERE tx_id=$2).

Message queue:

  • Хранить processed_message_ids в БД.
  • Использовать message_id из брокера (Kafka offset, RabbitMQ deliveryTag).
  • Outbox pattern для согласованности (см. файл 13 middle 1).

HTTP API:

  • Idempotency-Key header (Stripe-style).
  • PUT вместо POST там, где возможно.

Side effects (email, push):

  • Двухэтапный pattern: сохрани intent в БД с unique key, отправь в background worker, маркируй as sent.

Stripe — gold standard implementation. Что они делают:

Запрос:

POST /v1/charges HTTP/1.1
Host: api.stripe.com
Authorization: Bearer sk_test_...
Idempotency-Key: my-unique-key-123
Content-Type: application/x-www-form-urlencoded
amount=2000&currency=usd&source=tok_visa

Поведение Stripe:

  1. Если key новый — выполняет операцию, сохраняет result.
  2. Повторный запрос с тем же key + same body → cached response.
  3. Same key + different body → 422 idempotency_error.
  4. TTL — 24 часа.
  5. Не работает для GET (которые и так идемпотентны).
  6. Ключ scope’an per account.

Best practices от Stripe:

  • Использовать UUID v4.
  • Хранить key в client до получения final response (success или 4xx).
  • При network timeout — retry с тем же key.
  • Не использовать timestamp как key.

2.6.2 Идемпотентность для микросервисов — общий шаблон

Заголовок раздела «2.6.2 Идемпотентность для микросервисов — общий шаблон»

В микросервисной архитектуре каждый сервис должен быть идемпотентным consumer:

type IdempotentHandler[T any] struct {
store IdempotencyStore
fn func(ctx context.Context, msg T) error
}
func (h *IdempotentHandler[T]) Handle(ctx context.Context, msg Message[T]) error {
eventID := msg.ID // unique per event
// Begin tx
tx, err := h.store.BeginTx(ctx)
if err != nil { return err }
defer tx.Rollback()
// Check + reserve
locked, err := tx.TryLock(ctx, eventID)
if err != nil { return err }
if !locked {
// already processed by another consumer
return nil
}
// Process
if err := h.fn(ctx, msg.Payload); err != nil {
return err // tx rollback, retry next time
}
// Mark processed
if err := tx.MarkProcessed(ctx, eventID); err != nil { return err }
return tx.Commit()
}

Это абстрагирует idempotency для любого message handler.

G-Counter (Grow-only counter): Каждая нода хранит свой counter. Сумма всех counters = общее значение. Merge — поэлементный max.

type GCounter map[string]int // nodeID -> count
func (c GCounter) Increment(node string) { c[node]++ }
func (c GCounter) Value() int {
sum := 0
for _, v := range c { sum += v }
return sum
}
func (c GCounter) Merge(other GCounter) GCounter {
out := make(GCounter)
for k, v := range c { out[k] = v }
for k, v := range other {
if v > out[k] { out[k] = v }
}
return out
}

PN-Counter: G-Counter для increment + G-Counter для decrement. OR-Set (Observed-Remove Set): add/remove с unique tags. LWW-Register: значение + timestamp; merge — берёт более новое.

Используется в Redis CRDTs (Redis Enterprise), Riak, Automerge, Yjs.


⚠️ CAP не про “выбери 2 из 3” каждый день. Это про поведение при partition. Большую часть времени система работает в normal mode, и трейдоффы описываются PACELC.

⚠️ Linearizability дорогая. Каждая операция требует consensus (Raft, Paxos). Минимум 1 round-trip к majority. Latency 10-50ms.

⚠️ Eventual ≠ “eventually”. Без gossip/anti-entropy convergence может не случиться (потерянные обновления). Riak Read Repair, Cassandra hinted handoff — обязательны.

⚠️ Clock skew ломает LWW. NTP даёт ±50ms погрешность. Использование time.Now() для conflict resolution может потерять writes. Решение: hybrid logical clocks (HLC), vector clocks, или TrueTime (Spanner).

⚠️ Read-your-writes требует sticky session. Если read попал на replica без вашего write — баг. Решение: route к primary, или wait for replication lag, или session token с replica position.

⚠️ Idempotency-Key не равно request-id. Request-id — для трейсинга. Idempotency-Key — для дедупликации. Клиент генерирует его до первой попытки и переиспользует для retry.

⚠️ Different body, same key. Если клиент по ошибке использовал тот же key для другого запроса — отклонять с 422. Иначе вернёшь чужой ответ.

⚠️ In-progress race. Два параллельных request с одним key. Без блокировки — оба создадут заказ. С блокировкой — второй ждёт и видит результат первого.

⚠️ TTL для idempotency table. Без cleanup таблица растёт бесконечно. 24-48 часов — стандартный диапазон. Stripe — 24 часа.

⚠️ Не идемпотентные операции: auto-increment counter, time.Now(), генерация случайного ID внутри операции. Все они дают разный результат при повторе.

⚠️ At-least-once и dedup в Kafka. Exactly-once в Kafka работает только при enable.idempotence=true + transactional producer + read_committed consumer. Без этого — at-least-once + dedup в приложении.

⚠️ POST vs PUT в REST. Многие “PUT /users” роуты на самом деле создают нового user — это нарушение REST. PUT должен заменять ресурс полностью; идемпотентен.

⚠️ GET с побочными эффектами. “GET /counter/increment” — антипаттерн. Любая prefetch/bot заспамит. Используй POST.

⚠️ PATCH не идемпотентен. PATCH = частичное обновление. PATCH /counter {"op": "increment"} — повтор увеличит дважды. Используй PUT /counter {"value": 42} для идемпотентности.

⚠️ CDN кэширует GET. Если GET имеет side-effect — CDN не отправит origin при повторе. Кэшированный ответ выдаст.


Stripe — Idempotency-Key API. Все POST endpoints поддерживают Idempotency-Key. Stripe рекомендует UUID v4. Хранение 24 часа. При mismatch body — 422 error. Это стало де-факто стандартом для платёжных API. PayPal, Adyen, Square — все скопировали.

Yandex.Money / YooKassa. Также используют Idempotence-Key (через “e”). При повторе с тем же ключом возвращает первоначальный ответ. TTL — 24 часа.

Tinkoff Bank API. “Все запросы должны передавать поле Idempotency-Key, чтобы избежать дублирования операций при network errors”. Это критично для переводов.

Amazon DynamoDB. Eventually consistent reads по умолчанию (дешевле, быстрее). ConsistentRead=true — strong consistency, но 2x стоимость и latency. Это PACELC в действии.

Cassandra. Tunable consistency: для каждого запроса можно выбрать ONE, QUORUM, ALL. Quorum reads + Quorum writes = strong consistency (W+R > N). Используется в Netflix viewing history, Discord messages.

Google Spanner. Strong consistency + global distribution. Использует TrueTime API (atomic clocks + GPS) для linearizability. Подход — заплати latency (~7ms), получи ACID на глобальном уровне.

Kafka exactly-once semantics. Введена в Kafka 0.11 (2017). Включает: idempotent producer (sequence numbers), transactional API, read_committed isolation. Используется LinkedIn, Uber, Netflix для критичных pipelines.

GitHub webhooks retry. GitHub ретраит webhook delivery до 3 раз с exponential backoff при non-2xx response. Все webhooks включают X-GitHub-Delivery UUID, который handler должен использовать для дедупликации.

S3 read-after-write consistency (2020). До 2020 — eventual consistency для overwrite. После — strong read-after-write для всех PUT. AWS перешла на CP-like модель, что упростило приложения. Конкуренция с GCS, Azure.

Cloudflare R2. Strong consistency, no egress fees. PACELC: EC (latency higher than S3, but consistency wins).

MongoDB readPreference + write concern. writeConcern: { w: "majority" } — write на большинство реплик. readPreference: primary — read с primary. Combo = strong consistency. readPreference: nearest + w: 1 = eventually consistent, low latency.

Slack webhook delivery. Slack отправляет webhooks с at-least-once гарантией. X-Slack-Retry-Num и X-Slack-Retry-Reason headers — для понимания при retry. Endpoints должны быть идемпотентны. После 3 retries — drop.

Telegram Bot API webhooks. HTTPS endpoint обязателен. Telegram ретраит при non-200 status. Update_id уникальный — handler использует для дедупликации. Кроме того, Telegram offers getUpdates long polling как альтернативу webhook с offset-based consumption.

Notion API — eventual consistency view. Notion REST API имеет eventual consistency: write возвращает success, но read через 1-2 секунды может показать old data. Documentation предупреждает об этом, рекомендует retry/poll для critical reads.

Apache Kafka — exactly-once “marketing” vs reality. Когда Confluent объявили EOS в 0.11 — это было “internal Kafka EOS”. External side effects (БД writes, HTTP calls) не покрыты. Это часто непонимание у новичков: “Kafka EOS = я могу не думать об идемпотентности.” Нет.

DynamoDB conditional updates. DynamoDB поддерживает ConditionExpression: write выполняется только если условие выполнено. Реализует optimistic concurrency control. Например, UpdateItem с attribute_not_exists(processed) — атомарная “обработать только если не обработано”.

Linux NFS — early eventual consistency. NFS v3 — eventual consistency через client-side caching. Можно получить stale read после write от другого client. NFS v4 — close-to-open consistency, лучше но не linearizable.

git push с force vs idempotent rebase. git push идемпотентен (одинаковый push → одинаковый remote). git push --force — нет (зависит от прошлых пушей). git rebase — операция, повторное применение которой даёт тот же результат при одинаковом state.


Q1: Сформулируйте CAP теорему. A: В распределённой системе из Consistency, Availability, Partition tolerance можно сохранить только 2 из 3 при возникновении partition. На практике P неизбежен, выбор стоит между CP и AP.

Q2: Почему CA-система невозможна в распределённой среде? A: P (network partition) — данность, не выбор. Сеть всегда может разорваться. CA-система — это монолит на одной машине без репликации.

Q3: Что добавляет PACELC к CAP? A: PACELC описывает поведение в нормальном режиме (Else). Если нет partition — компромисс между Latency и Consistency. Strong consistency требует синхронной репликации, что повышает latency.

Q4: В чём разница между linearizability и serializability? A: Linearizability — про read/write одного объекта в real-time порядке. Serializability — про транзакции (multi-object): транзакции выглядят последовательными. Strict serializability = оба.

Q5: Что такое eventual consistency? Приведите пример. A: Гарантия, что при остановке writes все реплики сойдутся к одному состоянию. Примеры: DNS, S3 раньше, Cassandra. Подходит для счётчиков лайков, не подходит для переводов денег.

Q6: Что такое read-your-writes? A: Гарантия, что клиент после своего write видит этот write при следующем read. Реализуется через sticky session, чтение с primary, или session token с positions.

Q7: Что такое CRDT? A: Conflict-free Replicated Data Types — структуры данных с математически коммутативными операциями. Merge независим от порядка. G-Counter, OR-Set, LWW-Register. Используется в Riak, Yjs, Automerge.

Q8: ACID vs BASE — в чём разница? A: ACID — традиционный подход RDBMS: атомарность, согласованность, изоляция, durability. BASE — NoSQL: Basically Available, Soft state, Eventual consistency. Современные системы (Spanner) сочетают.

Q9: Что такое идемпотентность операции? A: Свойство f(f(x)) = f(x). Повторное выполнение даёт тот же результат. Критично для retries в распределённых системах.

Q10: Какие HTTP методы идемпотентны? A: GET, HEAD, OPTIONS, PUT, DELETE — идемпотентны. POST, PATCH — нет. Идемпотентный POST реализуется через Idempotency-Key header.

Q11: Как реализовать Idempotency-Key на сервере? A: Таблица idempotency_keys (key, request_hash, status, response). При запросе: проверяем key. Если есть completed — возвращаем сохранённый response. Если есть in_progress — 409. Если нет — INSERT, выполняем, COMPLETE.

Q12: Что делать, если запрос с тем же Idempotency-Key пришёл с другим телом? A: Возвращать 422 Unprocessable Entity. Это ошибка клиента: один key — одно намерение. Хранить SHA256 hash тела для detection.

Q13: Какой TTL для Idempotency-Key выбрать? A: Stripe — 24 часа, PayPal — 30 минут для некоторых, типично 24-48 часов. Запас на retry от клиента после реконекта.

Q14: Что произойдёт при двух параллельных запросах с одним Idempotency-Key? A: Без блокировки — race condition: оба выполнят операцию. Решение: advisory lock PostgreSQL или Redis SETNX или unique constraint + retry.

Q15: Можно ли использовать timestamp как Idempotency-Key? A: Нет. Timestamp может совпасть, может различаться между retries. UUID v4 — стандарт. Можно SHA256(business_data) — если бизнес-данные уникальны.

Q16: Что такое at-least-once delivery? Как с ней работать? A: Брокер гарантирует доставку хотя бы один раз, дубли возможны. Работать через идемпотентных consumer’ов: проверять processed_id перед обработкой.

Q17: Exactly-once в Kafka — миф или реальность? A: Реальность при правильной настройке: enable.idempotence=true + transactional producer + isolation.level=read_committed consumer. Но только внутри Kafka. Side effects (HTTP, БД) требуют отдельной идемпотентности.

Q18: Что такое vector clock? A: Структура для обнаружения causal relationship между событиями. Каждый процесс хранит счётчики версий всех процессов. При concurrent updates vector clocks несравнимы — конфликт.

Q19: Чем linearizability отличается от sequential consistency? A: Linearizability учитывает real-time порядок (если op B стартовала после завершения op A — B видит A). Sequential consistency — только порядок внутри процесса; real-time может нарушаться.

Q20: Почему clock skew опасен для LWW? A: Если node A имеет clock на 100ms впереди node B, то write на B с реальным timestamp может потеряться (его “побьёт” более старый write с A). Решение: HLC, vector clocks, atomic clocks (Spanner).

Q21: Когда eventual consistency допустима? A: Lightweight операции: counters, feeds, recommendations, search indexing. Где stale data на секунды-минуты приемлема. Не подходит: деньги, inventory, auth.

Q22: Как обеспечить идемпотентность отправки email? A: Сохранить intent (email_to_send) в БД с unique business_key. Background worker отправляет и маркирует as sent. При retry проверяем status. Уровень: at-most-once отправка.

Q23: Что такое read repair в Cassandra? A: Механизм: при read с несколькими репликами выявляются несоответствия. Background — отправляется correct value на stale реплики. Помогает eventual consistency сходиться.

Q24: Bounded staleness — что это? A: Гарантия, что read возвращает данные не старше T секунд / N версий. Промежуточный уровень между strong и eventual. Azure Cosmos DB, Spanner snapshot reads.

Q25: Какие проблемы у GET с побочным эффектом? A: Браузеры prefetch’ат GET, CDN кэшируют, web crawlers заспамят. GET должен быть safe. Side effects — только в POST/PUT/DELETE/PATCH.

Q26: Что такое hybrid logical clock (HLC)? A: Комбинация physical time и logical counter. Гарантирует monotonicity и approximation real-time. Используется CockroachDB. Решает проблему clock skew для distributed timestamps.

Q27: Чем write quorum (W) отличается от read quorum (R)? A: W — сколько реплик должны подтвердить write. R — сколько прочитать. W+R > N (N=total replicas) → strong consistency. Cassandra, Riak — tunable per-query.

Q28: Stripe Idempotency-Key TTL — почему 24 часа? A: 24 часа — баланс: достаточно длинно для клиента ретраить после network/maintenance issues, достаточно коротко для cleanup и privacy. Reasonable retry window для финансовых операций.

Q29: Можно ли использовать Idempotency-Key для GET? A: Нет смысла. GET идемпотентен по природе. Idempotency-Key — для POST/PATCH, где нужна дедупликация side effects.

Q30: Что такое monotonic writes consistency? A: Гарантия, что writes одного клиента применяются в порядке отправки. Если client пишет A потом B, любая реплика, видевшая B, видит и A. Слабее linearizability, но полезно для UX.


  1. Реализуй middleware Idempotency-Key. Используй PostgreSQL для хранения. Поддержи concurrent requests с advisory lock.

  2. Симулируй eventual consistency. Запусти 3 реплики (горутины) с асинхронной репликацией. Покажи stale reads. Добавь read repair.

  3. Напиши G-Counter CRDT. Тесты: merge коммутативен, ассоциативен, идемпотентен.

  4. Протестируй at-least-once с Kafka. Запусти consumer, который иногда падает после processing, но до commit offset. Покажи дубли. Добавь dedup через ID.

  5. Имитируй partition. Сетевой proxy между client и сервером с поддержкой “drop traffic”. Запусти 3 replica setup. Покажи, как ведут себя CP (etcd) и AP (Cassandra) при partition.

  6. Реализуй retry с exponential backoff и jitter для HTTP клиента. Используй cenkalti/backoff/v4. Покажи, как jitter снижает thundering herd.

  7. Сравни Idempotency-Key стратегии. Хеш body vs UUID от клиента. Когда какой подход лучше?

  8. Напиши тесты идемпотентности. Property-based: для случайных запросов второй раз возвращает то же, что первый.

  9. Реализуй PN-Counter CRDT. Тесты на конвергентность: 3 ноды делают increment/decrement, merge в любом порядке — одинаковый результат.

  10. Симулируй clock skew проблему. Два узла с timestamps, расходящимися на 1 минуту. LWW resolver. Покажи потерянные writes. Затем заменить на HLC.

  11. Webhook receiver с идемпотентностью. Имитируй GitHub-style webhook с дедупликацией по X-GitHub-Delivery UUID.

  12. Stale read demo. Запусти 2 instance Postgres (primary + read replica). Сделай write на primary, read с replica — покажи lag. Добавь read_after_write через session.

  13. Outbox idempotency. Реализуй consumer Kafka, который пишет в БД с дедупликацией через unique constraint на event_id.

  14. Bench: cost of strong consistency. Сравни latency и throughput Postgres single primary vs Postgres с sync replication к 2 replicas.


  1. Eric Brewer. “CAP Twelve Years Later: How the ‘Rules’ Have Changed.” IEEE Computer, 2012. — оригинальная переоценка CAP.
  2. Daniel Abadi. “Consistency Tradeoffs in Modern Distributed Database System Design.” IEEE Computer, 2012. — PACELC.
  3. Martin Kleppmann. “Designing Data-Intensive Applications.” O’Reilly, 2017. — главы 5, 7, 9 — must read.
  4. Stripe API Docs. “Idempotent Requests.” https://stripe.com/docs/api/idempotent_requests
  5. Werner Vogels. “Eventually Consistent.” ACM Queue, 2008. — основы от Amazon CTO.
  6. Pat Helland. “Idempotence Is Not a Medical Condition.” ACM Queue, 2012.
  7. Hewitt et al. “Cassandra Documentation.” — tunable consistency.
  8. Marc Shapiro et al. “Conflict-Free Replicated Data Types.” 2011. — CRDT foundation.
  9. Jepsen.io reports. https://jepsen.io/analyses — реальные тесты consistency баз данных.
  10. RFC 9110 — HTTP Semantics (методы и идемпотентность).