Locks control concurrent access to data during transactions. Without locks, multiple transactions reading and writing the same rows simultaneously can cause dirty reads, lost updates, and write skew.
Read lock (Shared) - FOR SHARE → I am reading, others can read too, but no one can write
Write lock (Exclusive) - FOR UPDATE → I am about to update, no one else can touch this row
Allows multiple transactions to read the same row at the same time, but blocks anyone from writing until all read locks are released.
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
Behavior:
| Who | Read | Write |
|---|---|---|
| Same transaction | ✅ Yes | ❌ No |
| Other transactions | ✅ Yes | ❌ Blocked |
When to use:
-- Audit report: lock all accounts in the department BEGIN; SELECT id, balance FROM accounts WHERE department = 'engineering' FOR SHARE; -- Other transactions can still read these rows -- but cannot UPDATE or DELETE them until COMMIT COMMIT;
Key point: Multiple transactions can hold a shared lock on the same row simultaneously. As soon as any transaction wants to write, it must wait for all shared locks to be released.
Blocks other transactions from both reading-with-lock and writing.
Plain SELECT (without FOR UPDATE or FOR SHARE) is not blocked.
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
Behavior:
| Who | Read | Write |
|---|---|---|
| Same transaction | ✅ Yes | ✅ Yes |
| Other — plain SELECT | ✅ Yes | — |
| Other — SELECT FOR SHARE | ❌ Blocked | ❌ Blocked |
| Other — SELECT FOR UPDATE | ❌ Blocked | ❌ Blocked |
| Other — UPDATE / DELETE | ❌ Blocked | ❌ Blocked |
When to use:
-- Debit: lock row, check balance, then deduct BEGIN; SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- Row is now exclusively locked -- Other FOR UPDATE / FOR SHARE / UPDATE must wait -- Check balance in application code -- If sufficient, proceed: UPDATE accounts SET balance = balance - 500 WHERE id = 1; COMMIT; -- Lock is released here
Important: Only one transaction can hold an exclusive lock on a row at a time. All other write-intent transactions are queued and run one after another.
Plain SELECT is never blocked:
-- While Transaction A holds FOR UPDATE on id=1: -- Transaction B — plain SELECT → goes through immediately SELECT balance FROM accounts WHERE id = 1; -- Transaction B — FOR UPDATE → blocked, must wait for A SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
By default, FOR UPDATE blocks and waits if a row is already locked.
MySQL provides two modifiers to change this behavior.
-- 1. Default — wait until lock is released (blocking) SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 2. NOWAIT — if row is locked, throw error immediately, do not wait SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT; -- 3. SKIP LOCKED — if row is locked, skip it and return next free row SELECT * FROM accounts WHERE id = 1 FOR UPDATE SKIP LOCKED;
Use when you want to try locking but fail fast if someone else already holds the lock. The application catches the error and responds immediately — no waiting.
BEGIN; SELECT balance FROM accounts WHERE id = 1 FOR UPDATE NOWAIT; -- If row is already locked by another transaction: -- ERROR 3572: Statement aborted because lock(s) could not be acquired -- Error thrown immediately — app catches and shows "please try again" COMMIT;
When to use:
Use when you want to skip locked rows and move on to the next available one. Perfect for job queues — multiple workers process rows in parallel without stepping on each other.
-- Worker picks the next available pending job -- Skips any rows already being processed by other workers BEGIN; SELECT * FROM jobs WHERE STATUS = 'pending' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; -- If top row is locked by Worker 2 → skipped automatically -- Returns the next unlocked row immediately COMMIT;
When to use:
| Syntax | Row is free | Row is locked |
|---|---|---|
FOR UPDATE | Lock and proceed | Wait until released |
FOR UPDATE NOWAIT | Lock and proceed | Throw error immediately |
FOR UPDATE SKIP LOCKED | Lock and proceed | Skip row, return next free |
// Default — wait for lock Account::lockForUpdate()->find(1); // NOWAIT — throw error if locked Account::lockForUpdate()->noWait()->find(1); // SKIP LOCKED — skip locked rows, return next free row Job::where('status', 'pending') ->orderBy('id') ->lockForUpdate() ->skipLocked() ->first();
Note:SKIP LOCKEDis the closest behavior to a plainSELECT—
instead of blocking, it moves on to the next unlocked row.
However it still acquires a lock on the row it returns —
it is not a lock-free read.
Shows whether two locks can be held simultaneously on the same row.
| Held ↓ / Requested → | FOR SHARE | FOR UPDATE |
|---|---|---|
| FOR SHARE | ✅ Compatible | ❌ Conflict |
| FOR UPDATE | ❌ Conflict | ❌ Conflict |
Deadlock occurs when two transactions each hold a lock the other needs, causing both to wait forever. MySQL detects this automatically and kills one transaction, returning an error.
Transaction A: locks row id=1, then needs row id=2 Transaction B: locks row id=2, then needs row id=1 Both wait for each other → deadlock
Prevention rule: always lock rows in a consistent order (ascending id).
-- RISK: inconsistent order -- Transaction A locks id=1 then id=2 -- Transaction B locks id=2 then id=1 -- → deadlock possible -- SAFE: always lock in ascending id order BEGIN; SELECT id, balance FROM accounts WHERE id IN (1, 2) ORDER BY id -- id=1 first, id=2 second — always FOR UPDATE; UPDATE accounts SET balance = balance - 500 WHERE id = 1; UPDATE accounts SET balance = balance + 500 WHERE id = 2; COMMIT;
Lock the entire table instead of individual rows. Rarely used in modern applications — row locks are preferred because table locks block all concurrent access to the whole table.
LOCK TABLES accounts READ; LOCK TABLES accounts WRITE; UNLOCK TABLES;
Behavior:
| Lock Type | Same Session | Other Sessions |
|---|---|---|
| READ | Read only | Read only (writes blocked) |
| WRITE | Read + Write | Fully blocked |
When to use:
-- Example: bulk export, ensure no writes during read LOCK TABLES accounts READ; SELECT * FROM accounts; -- No other session can INSERT, UPDATE, or DELETE -- Other sessions can still SELECT UNLOCK TABLES;
| Feature | Row Lock (FOR UPDATE / FOR SHARE) | Table Lock |
|---|---|---|
| Granularity | Single row | Entire table |
| Concurrency | High — only affected rows locked | Low — all rows blocked |
| Deadlock risk | Yes | No |
| Supported engines | InnoDB | InnoDB, MyISAM |
| Typical use | OLTP, web applications | Bulk operations, MyISAM |
| Lock | SQL | Blocks writes | Blocks FOR SHARE | Blocks plain SELECT |
|---|---|---|---|---|
| Shared (Read) | SELECT … FOR SHARE | ✅ Yes | ❌ No | ❌ No |
| Exclusive (Write) | SELECT … FOR UPDATE | ✅ Yes | ✅ Yes | ❌ No |
| Table Read | LOCK TABLES t READ | ✅ Yes | — | ❌ No |
| Table Write | LOCK TABLES t WRITE |