Seat holds without double booking, a read replica, PgBouncer and EXPLAIN
Two fans click the same two seats in the same millisecond. Both get "201 Created". Who goes to the show?
A: SELECT status → AVAILABLE B: SELECT status → AVAILABLE A: UPDATE … hold_id = A (locks the rows) B: UPDATE … hold_id = B (waits…) A: COMMIT → B's UPDATE runs → overwrites A
SELECT … ORDER BY id FOR UPDATE
UPDATE … WHERE version = :v
Variant: UPDATE … WHERE status = 'AVAILABLE' and check the row count.
UPDATE … WHERE status = 'AVAILABLE'
INVENTORY_LOCKING=naive docker compose up -d inventory bash scripts/reset-inventory.sh && k6 run load/flash-sale.js
GET /inventory/audit/double-bookings
Pessimistic and optimistic: 0. Naive: not 0.
Pessimistic
Optimistic
Either way, protect hot rows before the database: a waiting room (lab 7).
WITH expired AS ( UPDATE inventory.holds SET status = 'EXPIRED' WHERE id IN (SELECT id FROM inventory.holds WHERE status = 'ACTIVE' AND expires_at < now() LIMIT 500 FOR UPDATE SKIP LOCKED) RETURNING id) UPDATE inventory.seats SET status = 'AVAILABLE', hold_id = NULL WHERE hold_id IN (SELECT id FROM expired);
SKIP LOCKED
pg_basebackup -R
standby.signal
@Transactional(readOnly = true)
LazyConnectionDataSourceProxy
A replica adds read capacity. It does nothing for writes.
REPLICA_DELAY=1s docker compose up -d db-replica k6 run load/read-your-writes.js # stale_reads ↑
default_pool_size
Limit → Sort → Hash Join → Seq Scan on events (200,000 rows) ≈ 40 ms
CREATE INDEX events_starts_at_idx ON catalog.events (starts_at, id);
synchronous_commit
on
remote_apply
ANY 1
docker compose kill db
pg_promote()
db
pg_rewind
wal_log_hints
A human at 10:01 is too slow: Patroni + etcd decide the leader by consensus.
Outline: (1) the invariant; (2) the lost-update race; (3) three strategies; (4) the flash-sale test and audit; (5) streaming replica and routing; (6) read-your-writes; (7) PgBouncer; (8) indexes.