Chapter 06 · Data Modification, Transactions, Locking, and Concurrency Semantics

Isolation Levels, Consistent Reads, Locking Reads, MVCC, and Anomalies

See how concurrent sessions can read different versions of the same data, when InnoDB takes locks, and how isolation level changes snapshots, gap locking, and application-visible anomalies.

Beginner110–135 mintwo-session isolation labMySQL 8.4 LTS · current downloadable baseline 8.4.10MVCC + locking readsLast reviewed: August 2026

Learning outcomes

Concurrency becomes understandable when you separate two questions: which committed row version does my read see? and which rows or index ranges does my statement lock against other writers? InnoDB implements multi-version concurrency control (MVCC), meaning normal consistent reads can often read an earlier committed version without blocking a writer.

01

Compare READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, and SERIALIZABLE by observable behavior rather than names alone.

02

Distinguish consistent nonlocking reads from SELECT ... FOR SHARE and SELECT ... FOR UPDATE.

03

Explain InnoDB snapshots under READ COMMITTED and REPEATABLE READ.

04

Connect indexed locking searches to record, gap, and next-key locks where applicable.

05

Use two sessions and Performance Schema evidence to diagnose blocking without mistaking a snapshot read for a locking read.

Default baseline

InnoDB’s default isolation level is REPEATABLE READ. Do not change isolation globally just to make a tutorial “work.” Use SET SESSION TRANSACTION ISOLATION LEVEL ... in disposable sessions and understand the workload tradeoff first.

Standalone prerequisite for this lesson

If you arrived here without running Lessons 1–3, create servicehub_write_lab with the Lesson 1 setup first. The experiments assume InnoDB tables parts_inventory and work_orders. Open two or three independent mysql client sessions so each has its own transaction state.

MVCC mental model: current row versus visible row version

When a transaction updates an InnoDB row, undo information allows other transactions to reconstruct older committed versions as required by their read view. A normal SELECT is usually a consistent nonlocking read. A locking read (FOR SHARE or FOR UPDATE) asks for current rows with locks because the application intends to coordinate subsequent changes.

Read kindPurposeTypical concurrency effect
Consistent SELECTObserve a snapshot according to isolation rulesUsually does not wait for row X locks; may read an older committed version.
SELECT ... FOR SHARERead current qualifying rows and protect them against conflicting modificationAcquires shared locks; conflicting writers may wait.
SELECT ... FOR UPDATERead current qualifying rows with intent to updateAcquires exclusive-style record/range locks; competing modifiers wait.

REPEATABLE READ: one transaction, stable consistent-read snapshot

Use two sessions. First reset one inventory value:

sql · setup
UPDATE parts_inventory SET on_hand=20 WHERE part_id=1; COMMIT;

Session A:

sql · Session A · establish a REPEATABLE READ snapshot
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;START TRANSACTION;SELECT part_id,on_hand FROM parts_inventory WHERE part_id=1;-- expected: 20-- Leave the transaction open.

Session B:

sql · Session B · commit a concurrent change
UPDATE parts_inventory SET on_hand=19 WHERE part_id=1;COMMIT;SELECT part_id,on_hand FROM parts_inventory WHERE part_id=1;-- expected in B: 19

Back in Session A:

sql · Session A · repeat the normal consistent read
SELECT part_id,on_hand FROM parts_inventory WHERE part_id=1;-- expected: still 20 in the same REPEATABLE READ snapshotCOMMIT;SELECT part_id,on_hand FROM parts_inventory WHERE part_id=1;-- new transaction sees 19

This does not mean Session A locked the row. It means its consistent read uses a stable snapshot. Session B was able to update and commit.

READ COMMITTED: each consistent read gets a fresh snapshot

Repeat the experiment with Session A using READ COMMITTED. After Session B commits 19, the second SELECT in Session A sees the newly committed value because each consistent read gets its own snapshot.

sql · Session A · fresh snapshot per consistent read
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;START TRANSACTION;SELECT on_hand FROM parts_inventory WHERE part_id=1;-- Have Session B update and COMMIT here.SELECT on_hand FROM parts_inventory WHERE part_id=1;-- second result can reflect B's committed updateROLLBACK;

This exposes a nonrepeatable-read style effect by design. READ COMMITTED can improve some concurrency patterns, but it changes what application code can assume across repeated reads.

READ UNCOMMITTED and SERIALIZABLE: understand the ends of the spectrum

READ UNCOMMITTED permits dirty reads: a transaction can observe a row version another transaction has changed but not committed. It is rarely a sound default for business logic because the observed value can disappear on rollback. Demonstrate it only in this local lab.

SERIALIZABLE is the strongest standard isolation level. In InnoDB, when autocommit is disabled, plain SELECT statements are treated as locking reads in circumstances documented by MySQL, increasing blocking so concurrent execution behaves more like a serial order. Stronger isolation is not “free correctness”; it trades concurrency for stricter interaction rules.

Locking reads: when the next step depends on the current row

Suppose a dispatcher must reserve one part only if stock is positive. A consistent read followed later by an UPDATE leaves a race. Lock the row inside the same transaction:

sql · Session A · reserve a row with FOR UPDATE
START TRANSACTION;SELECT part_id,on_handFROM parts_inventoryWHERE part_id=1FOR UPDATE;UPDATE parts_inventorySET on_hand=on_hand-1WHERE part_id=1 AND on_hand>0;-- verify, then COMMIT or ROLLBACK

If Session B tries to update that same row before A commits, B waits for the conflicting lock. From a diagnostic session:

sql · Session C · inspect the wait relationship
SELECT *FROM performance_schema.data_lock_waits\GSELECT ENGINE_TRANSACTION_ID,THREAD_ID,OBJECT_NAME,INDEX_NAME,       LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATAFROM performance_schema.data_locksWHERE OBJECT_SCHEMA='servicehub_write_lab'  AND OBJECT_NAME='parts_inventory'ORDER BY ENGINE_TRANSACTION_ID,LOCK_STATUS;

The lock tables show a live relationship, not the business meaning of the operation. Your application still owns the invariant “stock must not become negative.”

Range locking: record, gap, and next-key behavior

InnoDB row locks are index-record locks. Under the default REPEATABLE READ isolation level, locking searches and index scans can use next-key locks: a record lock plus a lock on the preceding index gap. The purpose is to prevent another transaction from inserting a new row into a locked range and creating a phantom for a locking operation.

Create a small indexed queue:

sql · queue table for range-lock experiment
CREATE TABLE IF NOT EXISTS dispatch_queue (  job_id INT NOT NULL,  priority INT NOT NULL,  status VARCHAR(12) NOT NULL,  PRIMARY KEY(job_id),  KEY ix_dispatch_status_priority(status,priority,job_id)) ENGINE=InnoDB;DELETE FROM dispatch_queue;INSERT INTO dispatch_queue VALUES (10,1,'open'),(20,1,'open'),(30,2,'open');

Session A under REPEATABLE READ:

sql · Session A · locking range scan
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;START TRANSACTION;SELECT job_id,priorityFROM dispatch_queueWHERE status='open' AND priority=1FOR UPDATE;-- keep open

A concurrent insert that falls into the locked index range may wait because of gap/next-key locking. Under READ COMMITTED, gap locking is disabled for ordinary searches/index scans except cases such as foreign-key and duplicate-key checking. Do not summarize this as “READ COMMITTED has no gap locks whatsoever.”

Common mistake: expecting a normal snapshot SELECT to serialize a read-modify-write workflow

Snapshot is not reservation

A normal SELECT can show a stable historical version and still allow another transaction to change the current row. If the business operation requires reservation/coordination, use an atomic DML predicate or a locking read in the same transaction.

An even stronger pattern for simple inventory decrements can be one atomic statement:

sql · atomic conditional write
UPDATE parts_inventorySET on_hand=on_hand-1WHERE part_id=1 AND on_hand>0;SELECT ROW_COUNT() AS reserved_one;-- 1 means this statement changed one row; 0 means no qualifying stock row was available.

This removes the application-side read/write gap altogether.

Hands-on isolation matrix

  1. Run the REPEATABLE READ two-session snapshot experiment and record both sessions’ outputs.
  2. Repeat under READ COMMITTED and identify the changed second-read behavior.
  3. Use FOR UPDATE to create a lock wait and inspect it from a third session.
  4. Run the range-lock queue experiment under REPEATABLE READ, then repeat using READ COMMITTED and compare insert blocking.
  5. Finish every open transaction with COMMIT or ROLLBACK and restore the inventory seed.

Knowledge check

  1. Why can Session A read an old value while Session B has already committed a new one under REPEATABLE READ?
  2. Does a normal SELECT under REPEATABLE READ automatically prevent another session from updating the row?
  3. What changes for consistent reads under READ COMMITTED?
  4. What is a next-key lock?
  5. Why is SERIALIZABLE not automatically the best production isolation level?
Reveal answers
  1. A consistent read uses the transaction’s MVCC snapshot/read view and can reconstruct an older committed row version from undo information.
  2. No. A consistent nonlocking read is not a reservation. Use locking reads or atomic DML when coordination is required.
  3. Each consistent read receives a fresh snapshot, so repeated SELECTs in one transaction can see newly committed data.
  4. An index-record lock combined with a lock on the gap before that record, used under relevant locking searches to prevent inserts into the range.
  5. It can increase blocking and reduce concurrency; the correct level depends on business invariants and transaction design.

Anomaly map: name the behavior you are trying to prevent

A dirty read observes data another transaction has changed but not committed. Nonrepeatable read describes a transaction reading the same logical row twice and seeing a different committed value. A phantom is a newly qualifying or disappearing row in a repeated predicate/range query. These names are useful only when attached to a concrete application rule.

InnoDB’s MVCC and locking behavior means “phantom prevention” needs careful wording. Under REPEATABLE READ, ordinary consistent reads use a snapshot, so a repeated SELECT can remain stable without locking the range. Locking reads and DML need current rows and can use next-key locks to prevent inserts into qualifying index ranges. Those are different mechanisms serving different needs.

Also remember that a transaction always sees its own writes. A REPEATABLE READ transaction can therefore have a view that combines its own newer modifications with older snapshot versions of rows changed by other transactions. Do not describe MVCC as a frozen copy of the whole database; it is a visibility rule over row versions.

Isolation is one layer of correctness. Unique keys, CHECK constraints, foreign keys, atomic conditional UPDATE statements, explicit locks, and idempotency keys can encode stronger business invariants than relying on an isolation label alone.

Production judgment and next step

Choose isolation and locking from concrete anomalies your application must prevent, not from slogans about “stronger is better.” Keep transactions short, index locking predicates, and prefer atomic conditional DML when it can encode the invariant directly. Monitor lock waits because an otherwise correct transaction can still degrade under contention.

Lesson 5 turns that contention into deliberate experiments: a lock-wait timeout, a deterministic deadlock, diagnosis, lock ordering, and bounded idempotent retries.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.