MENU
Transactions, Locking, and Storage Engines
Two customers can buy the last copy of a book at the same moment, and a checkout that fails halfway leaves the database lying. Transactions make groups of statements all-or-nothing, and InnoDB's locks and row versions decide what concurrent transactions see of each other, on MySQL 9.7's defaults.The examples are transcripts of real runs, in which a script drove one mysql client per session: Session A> is a statement sent, Session A: what came back, and (waiting) a statement still blocked after half a second. Redundant acknowledgments (Query OK, 0 rows affected, 1 row in set) are omitted, and each run starts from a fresh bookshop, where product 5 has 7 copies.3.14.1 ACID and Why Transactions Matter
A transaction is a group of statements treated as one unit, with four guarantees known as ACID:Atomicity: all statements take effect or none; the undo log reverses them.
Consistency: keys, foreign keys and CHECK constraints (Section 3.4.3) always hold.
Isolation: transactions do not see each other's unfinished work, thanks to row locks and snapshots (Sections 3.14.4 to 3.14.6).
Durability: a commit survives a crash, because the redo log is flushed at COMMIT while innodb_flush_log_at_trx_commit keeps its default of 1.
Session A> START TRANSACTION;
Session A> UPDATE products SET stock = stock - 1 WHERE id = 1;
Session A: Query OK, 1 row affected
Session A> INSERT INTO order_items VALUES (1, 1, 1, 39.90);
Session A: ERROR 1062 (23000) at line 3: Duplicate entry '1-1' for key 'order_items.PRIMARY'
Session A> SELECT stock FROM products WHERE id = 1\G
Session A: *************************** 1. row ***************************
Session A: stock: 24
Session A> ROLLBACK;
Session A> SELECT stock FROM products WHERE id = 1\G
Session A: *************************** 1. row ***************************
Session A: stock: 25
3.14.2 START TRANSACTION, COMMIT, ROLLBACK, and SAVEPOINT
START TRANSACTION (or BEGIN) opens a transaction, COMMIT makes its changes permanent and visible, and ROLLBACK discards them. Modifiers include READ ONLY and WITH CONSISTENT SNAPSHOT (Section 3.14.5). Transactions do not nest: a second START TRANSACTION commits the first. A savepoint marks a point you can return to with ROLLBACK TO SAVEPOINT without abandoning the transaction; RELEASE SAVEPOINT forgets one, and COMMIT or ROLLBACK clears them all.A safe checkout decrements stock first, with the availability test inside the UPDATE. Its row lock makes a second buyer wait. Below, Session A also adds a free mug and then withdraws it with a savepoint; once A commits, B's UPDATE re-reads the committed row, finds 2 copies, and matches nothing:Session A> START TRANSACTION;
Session A> UPDATE products SET stock = stock - 5 WHERE id = 5 AND stock >= 5;
Session A: Query OK, 1 row affected
Session B> START TRANSACTION;
Session B> UPDATE products SET stock = stock - 5 WHERE id = 5 AND stock >= 5;
Session B: (waiting)
Session A> INSERT INTO orders (customer_id, ordered_at) VALUES (7, NOW());
Session A: Query OK, 1 row affected
Session A> INSERT INTO order_items VALUES (LAST_INSERT_ID(), 5, 5, 34.00);
Session A: Query OK, 1 row affected
Session A> SAVEPOINT gift;
Session A> UPDATE products SET stock = stock - 1 WHERE id = 7;
Session A: Query OK, 1 row affected
Session A> ROLLBACK TO SAVEPOINT gift;
Session A> COMMIT;
Session B: Rows matched: 0 Changed: 0 Warnings: 0
Session B> ROLLBACK;
Session A> SELECT id, stock FROM products WHERE id IN (5, 7);
Session A: +----+-------+
Session A: | id | stock |
Session A: +----+-------+
Session A: | 5 | 2 |
Session A: | 7 | 60 |
Session A: +----+-------+
3.14.3 Autocommit and Implicit Commits
With autocommit=1, the default, every statement outside START TRANSACTION commits the moment it succeeds. After SET autocommit = 0, every statement joins an open transaction that lasts until you commit, so a forgotten session holds its locks and its snapshot indefinitely. Leave autocommit on and open transactions explicitly, as PDO and Laravel do.Many statements cause an implicit commit of the open transaction: all DDL (CREATE, ALTER, DROP, RENAME, TRUNCATE), account statements (CREATE USER, GRANT), LOCK TABLES, START TRANSACTION, SET autocommit = 1, and maintenance such as ANALYZE TABLE. CREATE TEMPORARY TABLE does not commit, but cannot be rolled back either. In a test session with autocommit off, UPDATE products SET price = 5.49 WHERE id = 8, then CREATE TABLE price_audit (id INT), then ROLLBACK left the price at 5.49: the CREATE had already committed it. Keep schema changes out of data transactions, and do not expect a failed migration to undo its earlier DDL: each statement is atomic, but a sequence of them is not (Section 5.9.2).3.14.4 Isolation Levels and the Anomalies They Prevent
The SQL standard names three read anomalies. A dirty read sees another transaction's uncommitted change; a non-repeatable read gets a different value when it reads a row again; a phantom is a row that appears in a repeated range query. Four isolation levels trade them against concurrency; SET SESSION TRANSACTION ISOLATION LEVEL ... picks one, and @@transaction_isolation shows it.In this run Session B restocks one Databases title and adds another inside a transaction, while Session A sums the category three times:Session A> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Session A> START TRANSACTION;
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 2
Session A: SUM(stock): 12
Session B> START TRANSACTION;
Session B> UPDATE products SET stock = 20 WHERE id = 2;
Session B: Query OK, 1 row affected
Session B> INSERT INTO products VALUES (9, 'BK-SQL-03', 'Query Tuning', 3, 38.00, 5, NULL);
Session B: Query OK, 1 row affected
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 2
Session A: SUM(stock): 12
Session B> COMMIT;
Session A> SELECT COUNT(*), SUM(stock) FROM products WHERE category_id = 3\G
Session A: *************************** 1. row ***************************
Session A: COUNT(*): 3
Session A: SUM(stock): 25
Session A> COMMIT;
3.14.5 MVCC, Undo Logs, and Consistent Reads
Readers and writers share rows without blocking each other through multi-version concurrency control (MVCC). Each clustered-index record (Section 3.11.2) carries a hidden DB_TRX_ID, the last transaction that changed it, and DB_ROLL_PTR, a pointer to the previous version in the undo log. A plain SELECT is a consistent read: it walks back along the pointers to the newest version its read view, the set of transactions committed when the view was made, may see.
Figure 3.12: One row and its undo chain: each reader rebuilds the
version its read view allows (transaction IDs illustrative)Under REPEATABLE READ the first read creates the view, which lasts
until commit; under READ COMMITTED each statement gets a fresh one.
Writes and locking reads, however, act on the latest
committed version, because that is the one they lock:Session A> START TRANSACTION WITH CONSISTENT SNAPSHOT;
Session B> UPDATE products SET stock = stock - 2 WHERE id = 5;
Session B: Query OK, 1 row affected
Session A> SELECT stock FROM products WHERE id = 5\G
Session A: *************************** 1. row ***************************
Session A: stock: 7
Session A> UPDATE products SET stock = stock - 1 WHERE id = 5;
Session A: Query OK, 1 row affected
Session A> SELECT stock FROM products WHERE id = 5\G
Session A: *************************** 1. row ***************************
Session A: stock: 4
Session A> COMMIT;
3.14.6 Row Locks, Gap Locks, and Locking Reads
InnoDB locks index entries, so the index a statement scans decides what it locks. A record lock covers one entry, a gap lock the space between two entries, and a next-key lock (the REPEATABLE READ default) an entry plus the gap before it. An insert first requests an insert intention lock on its gap, which waits for other sessions' gap locks. Table-level IX and IS locks merely announce row locks. Plain SELECTs lock nothing, but SELECT ... FOR SHARE takes shared locks and SELECT ... FOR UPDATE exclusive ones. NOWAIT fails at once on a locked row, and SKIP LOCKED leaves locked rows out, which lets several workers share one job queue. This view lists the current database's locks:CREATE VIEW locks AS
SELECT engine_transaction_id AS trx, object_name AS tbl, index_name AS idx,
lock_mode AS mode, lock_status AS status, lock_data AS data
FROM performance_schema.data_locks
WHERE object_schema = DATABASE();
Session A> START TRANSACTION;
Session A> UPDATE products SET stock = stock + 1 WHERE category_id = 3;
Session A: Query OK, 2 rows affected
Session B> SET SESSION innodb_lock_wait_timeout = 3;
Session B> SELECT id FROM products WHERE stock > 0 ORDER BY id LIMIT 2 FOR UPDATE SKIP LOCKED;
Session B: +----+
Session B: | id |
Session B: +----+
Session B: | 1 |
Session B: | 4 |
Session B: +----+
Session B> SELECT id FROM products WHERE id = 2 FOR UPDATE NOWAIT;
Session B: ERROR 3572 (HY000) at line 3: Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set.
Session B> INSERT INTO products VALUES (9, 'BK-SQL-03', 'Query Tuning', 3, 38.00, 5, NULL);
Session B: (waiting)
Session C> SELECT * FROM locks ORDER BY trx, tbl, idx, data;
Session C: +-------+------------+-------------+------------------------+---------+------+
Session C: | trx | tbl | idx | mode | status | data |
Session C: +-------+------------+-------------+------------------------+---------+------+
Session C: | 92399 | products | NULL | IX | GRANTED | NULL |
Session C: | 92399 | products | category_id | X | GRANTED | 3, 2 |
Session C: | 92399 | products | category_id | X | GRANTED | 3, 3 |
Session C: | 92399 | products | category_id | X,GAP | GRANTED | 4, 5 |
Session C: | 92399 | products | PRIMARY | X,REC_NOT_GAP | GRANTED | 2 |
Session C: | 92399 | products | PRIMARY | X,REC_NOT_GAP | GRANTED | 3 |
Session C: | 92402 | categories | NULL | IS | GRANTED | NULL |
Session C: | 92402 | categories | PRIMARY | S,REC_NOT_GAP | GRANTED | 3 |
Session C: | 92402 | products | NULL | IX | GRANTED | NULL |
Session C: | 92402 | products | category_id | X,GAP,INSERT_INTENTION | WAITING | 4, 5 |
Session C: +-------+------------+-------------+------------------------+---------+------+
Session B: ERROR 1205 (HY000) at line 4: Lock wait timeout exceeded; try restarting transaction
Session A> ROLLBACK;
3.14.7 Table Locks, Metadata Locks, and the Global Read Lock
LOCK TABLES products WRITE, categories READ takes explicit table locks: READ lets every session read but none write, WRITE gives the holder sole access, and UNLOCK TABLES or START TRANSACTION releases them. MyISAM needs them; InnoDB transactions make them unnecessary.Every statement also takes a metadata lock (MDL) on each table it uses and holds it until its transaction ends, so no DDL can change the table underneath it. DDL needs an exclusive MDL, and a waiting exclusive request queues every later request behind it. In a test run, Session A updated one row of products and left its transaction open; B's ALTER TABLE products ADD isbn CHAR(13) then waited, and so did C's plain one-row SELECT, both shown in performance_schema.processlist as "Waiting for table metadata lock" until A committed. Before DDL on a busy table, look for old transactions in information_schema.INNODB_TRX, and lower lock_wait_timeout (a year by default) to a few seconds in the DDL session.FLUSH TABLES WITH READ LOCK takes the global read lock: every write on the server waits until UNLOCK TABLES, and START TRANSACTION does not release it. Backup tools hold it briefly to record a consistent binary log position. LOCK INSTANCE FOR BACKUP is lighter: it blocks DDL and other file-changing statements but lets DML run (Section 3.17 covers backups).3.14.8 Deadlocks: Detection, Diagnosis, and Avoidance
A deadlock is a cycle of waits: A holds a lock B needs while waiting for one B holds. Two carts that contain the same two books, updated in opposite order, are enough:Session A> START TRANSACTION;
Session A> UPDATE products SET stock = stock - 1 WHERE id = 1;
Session A: Query OK, 1 row affected
Session B> START TRANSACTION;
Session B> UPDATE products SET stock = stock - 1 WHERE id = 2;
Session B: Query OK, 1 row affected
Session A> UPDATE products SET stock = stock - 1 WHERE id = 2;
Session A: (waiting)
Session B> UPDATE products SET stock = stock - 1 WHERE id = 1;
Session A: Query OK, 1 row affected
Session B: ERROR 1213 (40001) at line 3: Deadlock found when trying to get lock; try restarting transaction
Session A> COMMIT;
mysql -E -e 'SHOW ENGINE INNODB STATUS' \
| grep -E '^(\*\*\* |UPDATE |RECORD LOCKS)' \
| sed -E 's/space id .* (index \w+) of table (\S+) trx id [0-9]+/\1 \2/'
*** (1) TRANSACTION:
UPDATE products SET stock = stock - 1 WHERE id = 2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS index PRIMARY `shop`.`products` lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS index PRIMARY `shop`.`products` lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
UPDATE products SET stock = stock - 1 WHERE id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS index PRIMARY `shop`.`products` lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS index PRIMARY `shop`.`products` lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (2)
3.14.9 XA Distributed Transactions
An XA transaction is one branch of a transaction spanning several resource managers, such as two MySQL servers. It follows the X/Open XA two-phase commit: a transaction manager sends XA PREPARE to every branch (the work becomes durable but undecided), then XA COMMIT once all have prepared, or XA ROLLBACK if any failed. A branch runs between XA START 'id' and XA END 'id'; only InnoDB supports XA.Since MySQL 8.0.29 a prepared branch is detached from its connection (xa_detach_on_prepare, on by default), so it survives a disconnect and any session can finish it: in a test run, Session B saw Session A's prepared branch in XA RECOVER and committed it with XA COMMIT. A prepared branch keeps its row locks until someone decides its fate. XA is unsafe for statement-based replication, and detached XA transactions cannot use temporary tables. PHP applications rarely need XA: keep related data in one database, or write an outbox table in the local transaction and deliver its messages afterwards.3.14.10 Storage Engines: InnoDB, MyISAM, MEMORY, and the Rest
MySQL's storage engines are pluggable: each table names its engine in ENGINE= (Section 3.4.5), and one query may join tables that use different engines. SHOW ENGINES lists what the server offers; the same data in information_schema.ENGINES lets you pick columns and rows:SELECT engine, support, transactions, xa, savepoints
FROM information_schema.ENGINES WHERE support <> 'NO' ORDER BY engine;
+--------------------+---------+--------------+------+------------+
| engine | support | transactions | xa | savepoints |
+--------------------+---------+--------------+------+------------+
| ARCHIVE | YES | NO | NO | NO |
| BLACKHOLE | YES | NO | NO | NO |
| CSV | YES | NO | NO | NO |
| InnoDB | DEFAULT | YES | YES | YES |
| MEMORY | YES | NO | NO | NO |
| MRG_MYISAM | YES | NO | NO | NO |
| MyISAM | YES | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | NO | NO | NO |
+--------------------+---------+--------------+------+------------+
8 rows in set (0.000 sec)