Intermediate: combining and changing data · Changing data, transactions and ACID · topic 41 of 62 · 40 min
ACID, isolation and locking
Explanation
ACID with the last Tatkal berth
At 10 a.m. thousands of people try to book the same few Tatkal berths. A booking system like this must guarantee four things, known together as ACID.
| Letter | Promise | In the berth booking |
|---|---|---|
| A: Atomicity | All statements of a transaction happen, or none | Taking the payment and marking the berth as booked happen together; never one without the other |
| C: Consistency | Each transaction moves the data from one valid state to another; constraints always hold | A berth has at most one passenger; seats_left never goes below 0 |
| I: Isolation | Transactions running at the same time do not see each other's unfinished work | Two users clicking at the same second cannot both get the last berth |
| D: Durability | Once COMMIT returns, the change survives a crash or power cut | A confirmed ticket is still there after the server restarts; the database wrote it to its log on disk first |
What goes wrong without isolation
When transactions run at the same time, four classic problems, called anomalies, can appear:
- Dirty read: you read a change that another transaction has not committed yet, and it later rolls back. You acted on data that never existed.
- Non-repeatable read: you read a row twice in one transaction and get different values, because someone committed an update in between.
- Phantom read: you run the same WHERE twice and get a new row the second time, because someone inserted a matching row.
- Lost update: two transactions read the same value, both compute a new one, and the second write overwrites the first. Two people read seats_left = 1, both write 0, and two tickets are sold for one seat.
Isolation levels
Full isolation costs speed, so SQL defines four isolation levels. Each one stops more anomalies than the one before.
| Level | Dirty read | Non-repeatable read | Phantom |
|---|---|---|---|
| READ UNCOMMITTED | possible | possible | possible |
| READ COMMITTED | prevented | possible | possible |
| REPEATABLE READ | prevented | prevented | possible (by the standard) |
| SERIALIZABLE | prevented | prevented | prevented |
Defaults differ: PostgreSQL, SQL Server and Oracle use READ COMMITTED; MySQL with InnoDB uses REPEATABLE READ. PostgreSQL treats READ UNCOMMITTED as READ COMMITTED, and its REPEATABLE READ also blocks phantoms. SQLite allows only one writer at a time, so its transactions behave as SERIALIZABLE. At SERIALIZABLE, PostgreSQL may cancel one of two clashing transactions with a "could not serialize access" error, and the app must simply retry it.
-- PostgreSQL
BEGIN ISOLATION LEVEL SERIALIZABLE;
-- ... statements ...
COMMIT;Locks and SELECT ... FOR UPDATE
Databases protect rows with locks. A row lock taken by an UPDATE makes other writers of the same row wait until the first transaction commits or rolls back. When you must read a row and then decide, lock it while reading with SELECT ... FOR UPDATE (PostgreSQL, MySQL, Oracle). SQLite has no FOR UPDATE; BEGIN IMMEDIATE takes the write lock for the whole database instead. SQL Server uses table hints such as WITH (UPDLOCK).
-- PostgreSQL / MySQL
BEGIN;
SELECT status FROM berths WHERE id = 2 FOR UPDATE; -- other bookers wait here
UPDATE berths SET status = 'Booked', booked_by = 'Riya' WHERE id = 2;
COMMIT;Even simpler: make the check part of the UPDATE itself. UPDATE quota SET seats_left = seats_left - 1 WHERE train_no = '12951' AND class = '3A' AND seats_left > 0 is one atomic statement. If it changes 0 rows, the seat is gone.
Optimistic locking with a version column
Locks are pessimistic: they assume a clash and make others wait. Web apps often use optimistic locking instead. Each row has a version number. The app reads the row and its version, and later updates only if the version has not changed, bumping it at the same time.
UPDATE berths
SET status = 'Booked', booked_by = 'Riya', version = version + 1
WHERE id = 2 AND version = 2;
-- changes() = 1: Riya wins, version is now 3
-- Sameer's identical update with version = 2 now changes 0 rows: "Sorry, just booked"Deadlocks
A deadlock happens when transaction A holds a lock that B needs while B holds a lock that A needs; both wait forever. Databases detect this and cancel one of them (PostgreSQL reports "deadlock detected"). Prevent most deadlocks by always locking rows in the same order, for example the lower account id first, and by keeping transactions short. The app should retry a transaction cancelled by a deadlock.
Common mistakes
- Read-modify-write in app code (read seats_left, subtract 1 in Python, write it back) without a lock or a version check. Use an atomic UPDATE, FOR UPDATE or a version column.
- Not checking the row count of an optimistic update. 0 rows means someone else won; tell the user and reload.
- Assuming every engine has the same default isolation level. Check it.
- Raising everything to SERIALIZABLE without retry logic. Expect serialization errors and retry.
- Locking rows in a different order in different code paths, which invites deadlocks.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
ACID, isolation and locking · 1/8
At ten in the morning, thousands of people try to book the same few Tatkal berths. ACID is what keeps that fair and correct.
Prefer a video by a teacher? Find free videos on “acid transactions isolation levels dirty read phantom lost update explained” on YouTube ↗
In real life: Selling the last seat exactly once
Ticketing systems for trains, flights, cinemas and concerts all face the same rush: many buyers, few seats. A common design keeps a counter or a row per seat, changes it only with a guarded UPDATE (seats_left > 0, or a matching version), checks the affected row count, and takes the payment and the seat in one transaction. If the count is 0, the buyer sees "no longer available" instead of a double booking.
UPDATE quota SET seats_left = seats_left - 1
WHERE train_no = '12951' AND class = '3A' AND seats_left > 0;
SELECT changes(); -- 1 for the first buyer, 0 for the secondPractice (0/4 done)
Optimistic locking. Riya and Sameer both opened berth id 2 when its version was 2. Riya's update arrives first: set status 'Booked', booked_by 'Riya' and version = version + 1, only WHERE id = 2 AND version = 2. Then run Sameer's update with the same old version: status 'Booked', booked_by 'Sameer', version + 1, WHERE id = 2 AND version = 2. Finally return id, status, booked_by and version of berth 2.
Train 12951 has 1 seat left in class '3A'. Two buyers try to book at once. Write the booking as one guarded UPDATE that takes a seat only if seats_left > 0, and run it twice (once per buyer). Then return train_no, class and seats_left for class '3A'. The seat count must never go below 0.
Two clerks each read stock = 10, each sells 1 item, and each writes back stock = 9 from their app. The real stock should be 8. Which anomaly is this, and what is a simple fix?
Your app runs on MySQL with InnoDB and you have not changed any setting. Which isolation level are your transactions using?
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Upserts: insert or updateChanging data, transactions and ACID · same module
- Transactions and ACIDBackend Development · SQL in depth
- Theory questions you will be askedInterview questions and practice projects
- NoSQL: documents, key-value, wide-column and graphProcedures, triggers, SQL engines and NoSQL
- Race conditions and synchronizedJava Programming (Core to Spring Boot) · Multithreading and concurrency
- What is a database?Backend Development · Databases and SQL basics
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Which ACID property promises that a committed booking survives a power cut?
Answer: Durability
Durability: once COMMIT returns, the change is safely on disk.
Transaction A reads a balance that B has changed but not committed; B then rolls back. What is this?
Answer: A dirty read
Reading uncommitted data is a dirty read. READ COMMITTED and above prevent it.
What is the default isolation level of PostgreSQL?
Answer: READ COMMITTED
PostgreSQL, SQL Server and Oracle default to READ COMMITTED.
An optimistic-locking UPDATE ... WHERE id = 2 AND version = 5 changes 0 rows. What does it mean?
Answer: Someone else changed the row after you read it; reload and try again
The version moved on, so your copy is stale.
Which statement locks the rows you read until your transaction ends, in PostgreSQL and MySQL?
Answer: SELECT ... FOR UPDATE
FOR UPDATE takes row locks. SQLite does not support it.
How do you make deadlocks less likely?
Answer: Lock rows in the same order everywhere and keep transactions short
A consistent lock order removes the circular wait; a short transaction holds locks briefly.
Which ACID property means "after the app shows Payment successful, a power cut cannot undo it"?Related
Answer: Durable
Durable: once committed, the change is safely stored and survives crashes.
Which isolation level prevents dirty reads but still allows non-repeatable reads?Related
Answer: Read Committed
Read Committed only shows committed data, but a row can change between two reads.
Since which version does MongoDB support multi-document ACID transactions (on replica sets)?Related
Answer: 4.0
MongoDB 4.0 added multi-document transactions on replica sets, and 4.2 extended them to sharded clusters.
Inside a transaction you run SAVEPOINT s1, then an UPDATE, then ROLLBACK TO s1. What happens?Related
Answer: Only the work after s1 is undone, and the transaction stays open
ROLLBACK TO undoes back to the marker and keeps the earlier work pending.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Changing data, transactions and ACID”. The module exam comes next.
- Module 8 exam: Changing data, transactions and ACID · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Schema design and normalisation. 62 topics left in the course before the final exam.