Module 2 · Databases

Lesson 12 — NoSQL

Redis, MongoDB and search engines: what you sacrifice with each one and when it pays off.

Published
In this lesson
  1. Exercise 1 — Cache
  2. Exercise 2 — Rate limit
  3. Exercise 3 — Lock
  4. Exercise 4 — JSONB
  5. Exercise 5 — Stampede
  6. Professor's summary

Exercise 1 — Cache

  1. First call: heavy query; the rest: Redis (~sub-ms, 0 queries). The metric that matters: the endpoint's p99 drops from tens of ms to <2ms.
  2. Stale data ≤10s: acceptable for a public listing; NOT for checkout (there the DB with locks rules — Lesson 10). Defensible TTL: 5-10s for the listing; 0 (no cache) in the purchase flow.
  3. Invalidation gives instant freshness after a reservation; it introduces the stampede: all readers hit the DB at once after the DELETE. Antidotes: jitter, soft-TTL, worker regeneration (29).

Exercise 2 — Rate limit

1-2. count = r.incr(key, ex=60); if count == 1 the TTL was just set (ex in the same command: atomic). Requests 21+ receive 429.

  1. Without atomicity, two threads read the same 19, both write 20, both pass: N requests over the limit under concurrency. INCR is atomic because Redis is single-threaded: the "read-add-write" row is never interrupted (Lessons 03/10 applied to Redis).

Exercise 3 — Lock

  1. The loser waits up to 2s (blocking_timeout) and gets LockError/LockNotOwnedError → 409 "card in use". The winner redeems.
  2. Without the TTL, the dead pod's lock lives forever: the card becomes unredeemable eternally (a ghost block). The TTL makes the lock self-cleaning — the price: if your live process takes longer than the TTL, two processes can believe they hold the lock (hence: short operations inside the lock + renew if needed).
  3. The Redis lock serializes the redemption; data atomicity comes from the DB transaction (10): ledger INSERT + UPDATE inside the transaction. Lock + transaction: two tools, each for its layer.

Exercise 4 — JSONB

  1. Migration: AddField JSONB default '{}' (safe) + the GIN RunSQL with CONCURRENTLY (11).
  2. Event.objects.filter(metadata__contains={"venue_type": "stadium"}) generates @>; the plan uses a Bitmap Index Scan over the GIN.
  3. Deserves a real column: what you ALWAYS filter and/or constrain (starts_at is already a column). Stays in JSONB: organizer preferences (decoration, optional, no constraints).

Exercise 5 — Stampede

  1. Without a hot cache: ~50 queries (all of them lose the race). 2. With jitter, expirations spread out and regeneration queries drop to 2-5 (those coinciding in a window). The regeneration lock takes it to 1 — but its complexity is only justified if the recomputation is expensive.

Professor's summary

  • Postgres primary; Redis for latency/atomicity/TTL; JSONB for semi-structured; Mongo/ES only with a metric justifying them.
  • INCR/SET NX are atomic because of single-threading: your concurrency is solved inside the data server.
  • A lock without TTL = an eternal block after a crash. TTL + short operation.
  • The stampede is avoided with jitter (free) and single regeneration (when recomputation hurts).

Data module closed. After the correction: Lesson 13 — REST done right (Module 3).