ExerciseWarm-up
Warm-up
Reproduce all four anomalies
10 minjunior0–15 yrs
One concept, guided. Near-impossible to fail.
What this teaches
- An isolation level is a rule about which versions a read can see
- The standard states a minimum each level must prevent, not a ceiling
- A lost update needs none of the three named anomalies to occur
- Your database's default is probably not the one you assumed
Starter
Starter.java
import java.util.*;
/**
* Warm-up: build a store just big enough to show why the levels differ.
*
* No database and no threads. Two transactions interleaved in an order you
* write by hand — which is the only way to see these anomalies reliably,
* because in a real system they depend on timing you do not control.
*/
public class Starter {
enum Level { READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE }
/** One value written by one transaction. Invisible to others until committed. */
static final class Version {
final String key; final int value; final long writer;
long committedAt = Long.MAX_VALUE;
Version(String key, int value, long writer) {
this.key = key; this.value = value; this.writer = writer;
}
}
public static void main(String[] args) {
// TODO 1: build the store. A List<Version> and a counter that stands in
// for time is enough. seed(key, value) adds an already-committed
// version; begin(level) hands back a transaction stamped with the
// current clock.
// TODO 2: write read(key). This one method is the whole exercise —
// every level is a different answer to "which versions may I see?":
//
// READ_UNCOMMITTED any version at all, committed or not
// READ_COMMITTED any version committed by the time of THIS read
// REPEATABLE_READ only versions committed before I started
// SERIALIZABLE same, plus a check at commit time
//
// A transaction must always see its own uncommitted writes.
// TODO 3: dirty read. Transaction A writes without committing;
// transaction B reads. Run it at READ_UNCOMMITTED and READ_COMMITTED.
// Say what B risks acting on if A later rolls back.
// TODO 4: non-repeatable read. B reads, A writes and commits, B reads
// the same key again. Run at READ_COMMITTED and REPEATABLE_READ.
// Nothing broke a rule here — explain why that is the point.
// TODO 5: phantom. Add readRange(prefix) returning matching keys. B
// scans, A inserts a new key and commits, B scans again.
//
// Your REPEATABLE_READ will prevent this. The SQL standard PERMITS
// phantoms at that level. Explain why both statements are correct, and
// which real database behaves like your model.
// TODO 6: lost update. A and B both read 100, both write 150, both
// commit. Print the balance. Which of the three anomalies above did
// this require? Read the question again — the answer is none.
// TODO 7: make SERIALIZABLE refuse the second commit. Record what each
// transaction read, and at commit time reject if anything it read was
// written and committed by someone else after it began. Confirm B is
// now rejected rather than silently overwriting A.
// TODO 8: with B rejected, the balance is 150 and still not 200. Say
// what the application must now do, and why serializable on its own
// does not make the code correct.
// TODO 9: run all four anomalies at all four levels and print the
// matrix. Compare it to the table in any textbook. Two cells differ —
// find them and explain both.
// TODO 10: finally, check reality. Run the query for your own database
// (SELECT current_setting('transaction_isolation') on PostgreSQL,
// SELECT @@transaction_isolation on MySQL). Was it what you expected?
}
}Run it locally:
cd exercises/java/transactions/isolation-levels/01-warmup
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterDone when
- You produced a dirty read, a non-repeatable read, a phantom and a lost update
- You can say which level first prevents each one
- You showed that snapshot reads prevent phantoms the standard permits
- You checked what your own database actually defaults to
← Back to What are the isolation levels, and what does each permit?