Module 2 · Databases

Lesson 10 — Transactions

ACID, isolation levels, locks and optimistic/pessimistic concurrency in seat reservation.

Published
In this lesson
  1. Exercise 1 — The race, reproduced
  2. Exercise 2 — select_for_update: exclusion works
  3. Exercise 3 — Deadlock: provoke it and fix it
  4. Exercise 4 — Isolation level in action
  5. Exercise 5 — The definitive transaction
  6. Submit

Django shell + threading (Lesson 03 gave you the muscle). Do not look at solutions.md before submitting.

Setup: python manage.py shell with two free seats and two users.

Exercise 1 — The race, reproduced

  1. Write reserve_without_lock(seat_id, user): read is_reserved(), sleep 0.05s, create the item. Run it with 2 threads on the same seat. How many reservations were created? (the I1 constraint may save the data with IntegrityError — catch it and note which thread won).
  2. Remove the conditional constraint (temporary migration) and repeat: 2 reservations? Document the disaster and restore.

Exercise 2 — select_for_update: exclusion works

  1. Same function, but with Seat.objects.select_for_update().get(pk=...) inside transaction.atomic(). What does thread 2 do? How many reservations at the end?
  2. Switch to nowait=True: which exception reaches thread 2? How do you translate it into an HTTP response?
  3. With skip_locked=True and 2 threads reserving different seats of the same event: did each reserve theirs without waiting on each other?

Exercise 3 — Deadlock: provoke it and fix it

  1. Thread A locks seat 1 then seat 2 (two separate transactions, with sleeps). Thread B: first 2, then 1. Does PostgreSQL kill anyone? What exact error?
  2. Fix it with a global order (both lock with order_by("id")). Does it still happen?
  3. Add automatic retry: catch the deadlock error and retry once with backoff.

Exercise 4 — Isolation level in action

  1. On READ COMMITTED (default): thread A starts a transaction and reads an event's price; thread B changes it and commits; A re-reads inside its transaction. Did it see the change? (non-repeatable read).
  2. Repeat with REPEATABLE READ (transaction.atomic() + SET TRANSACTION ISOLATION LEVEL REPEATABLE READ). What does A read now?
  3. Write the rule: when you raise the level and when a point lock suffices.

Exercise 5 — The definitive transaction

Implement the lesson's full reserve() (ordered locking + rules + creation + I2) and demonstrate it:

  1. 5 threads, 5 different seats: all 201, no perceptible waits.
  2. 2 threads, same seat: 1 winner and 1 with your domain exception → 409.
  3. Final metric: len(connection.queries) of one reservation (should be a small, stable number) and the total time of the 5 reservations from point 1.

Submit

Paste code, outputs and numbers. With this we close the heart of the data module; next Lesson 11 — Versioned migrations.