open atlas
↑ К треку
Разборы System Design SDC · 05 · 09

Данные и деньги: чтение кода и конфига

Читай реальный код и конфиг из кейсов данных-и-денег, затем выбери senior-починку: лейбл метрики, идемпотентное списание, дебет кошелька с потерянным обновлением и атомарную многоночную бронь.

SDC Senior ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Денежные баги живут в коде, не в прозе: лейбл на метрике, неатомарное чтение-затем-запись, списание без ключа идемпотентности, поночёвый декремент вне транзакции. Читай каждый сниппет, проследи отказ и выбери починку, что закоммитил бы senior-инженер.

Практикуй петлю, что гоняешь в дизайн-ревью по деньгам или критичным к корректности данным: засеки угрозу в коде, назови её и выбери изменение, что механизм реально требует.

Сниппет 1 — лейбл метрики

# инструментация хендлера запросов
REQUESTS = Counter("http_requests_total", labels=["route", "status", "user_id"])

def handle(req):
    REQUESTS.labels(route=req.route, status=resp.status, user_id=req.user_id).inc()
Викторина

Что не так с этой инструментацией и какова починка?

Сниппет 2 — вызов списания

def checkout(order):
    # клиент повторяет этот вызов при таймауте
    resp = psp.charge(amount=order.total, source=order.card)
    ledger.record(order.id, resp)
    return resp
Викторина

Клиент повторяет checkout() после таймаута. Что произойдёт и какова починка?

Сниппет 3 — дебет кошелька

def debit(wallet_id, amount):
    bal = db.query("SELECT balance FROM wallets WHERE id = ?", wallet_id)
    if bal >= amount:
        db.execute("UPDATE wallets SET balance = ? WHERE id = ?", bal - amount, wallet_id)
        return "ok"
    return "insufficient"
Викторина

Два конкурентных дебета бьют по тому же кошельку. Каков баг и однооператорная починка?

Сниппет 4 — многоночная бронь

def reserve(room_type, nights):  # nights = [Dec31, Jan1, Jan2]
    for night in nights:
        db.execute(
          "UPDATE inventory SET booked = booked + 1 "
          "WHERE room_type = ? AND date = ? AND booked < total",
          room_type, night)
    return "reserved"   # каждая ночь обновлена независимо, без транзакции
Викторина

Поночёвый UPDATE условный (хорошо), но у цикла нет транзакции. Что может пойти не так и какова починка?

Вспомните перед уходом
  1. 01
    Как читать сниппет дебета кошелька или брони на угрозу потерянного обновления / частичной записи?
  2. 02
    Как засечь угрозы идемпотентности и кардинальности в коде?
Итог

Каждый баг данных-и-денег читается прямо с кода. Неограниченный лейбл метрики (user_id) форкает ряд на значение и OOM’ит TSDB — кардинальность рядов, не темп запросов, ограничение, так что убери его из лейблов. Списание, что клиент повторяет без ключа идемпотентности, списывает дважды, когда первый ответ потерян — передай клиентом отчеканенный ключ с атомарным check-and-store. Дебет кошелька, что читает баланс, затем пишет обратно, имеет зазор чтения-записи, что эксплуатируют два конкурентных дебета (потерянное обновление) — схлопни в один атомарный условный UPDATE. И многоночная бронь, что декрементит каждую ночь вне транзакции, может оставить частичную бронь — оберни весь диапазон в одну транзакцию всё-или-ничего. Senior-привычка одна для всех четырёх: найди угрозу, назови её и сделай операцию ограниченной (кардинальность) или атомарной (идемпотентность, потерянное обновление, частичная запись) — починку, что требует механизм, не ту, что её прячет.

Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.