MySQL "Deadlock found when trying to get lock": read the report, retry, and stop the cycle
Fix MySQL error 1213 "Deadlock found when trying to get lock": read SHOW ENGINE INNODB STATUS, retry the transaction, lock rows in the same order and add missing indexes.
by OhSudo ··
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transactionTwo transactions each hold a lock that the other one needs, so neither can continue. InnoDB notices at once, picks a victim, rolls it back, and lets the other one finish.
So this error is not a sign of a broken database. The database solved the problem, and told your application to try again, which is why the message says “try restarting transaction”. An occasional deadlock is normal on a busy system. Your job is to make sure the application retries, and that they stay rare.
- Transaction 1holds row 1, wants row 2waits for
- Transaction 2holds row 2, wants row 1waits for the other(fails)
- InnoDBrolls one back: ERROR 1213(breaks here)
TL;DR
- Your code must retry the whole transaction on error 1213.
- To see what collided:
- To make them rarer: lock rows in the same order everywhere, keep transactions short, and add the index the query is missing.
What a deadlock looks like
Transaction 1 Transaction 2UPDATE accounts ... WHERE id = 1; UPDATE accounts ... WHERE id = 2;UPDATE accounts ... WHERE id = 2; -- waits for transaction 2 UPDATE accounts ... WHERE id = 1; -- ERROR 1213: chosen as the victimIt’s the classic: the same two rows, in opposite orders. Only the victim’s transaction is rolled back, all of it, and not just the last statement.
1. Read the report
Find the section LATEST DETECTED DEADLOCK:
*** (1) TRANSACTION:TRANSACTION 5401, ACTIVE 8 sec starting index readUPDATE accounts SET balance = balance - 10 WHERE id = 2*** (1) HOLDS THE LOCK(S):RECORD LOCKS space id 2 page no 4 n bits 72 index PRIMARY of table `shop`.`accounts` trx id 5401 lock_mode X locks rec but not gap*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 2 page no 4 n bits 72 index PRIMARY of table `shop`.`accounts` trx id 5401 lock_mode X locks rec but not gap waiting*** (2) TRANSACTION:...*** WE ROLL BACK TRANSACTION (2)Look at the two queries, the table, the index named, and the lock mode. lock_mode X locks gap before rec or next-key tell you it’s locking ranges, not only single rows. Often that’s the key to the cause.
It only keeps the latest one. To get them all in the error log:
2. Retry in your code
Every application using transactions on a busy database needs this. Retry the whole transaction, wait a little, and give up after a few tries:
for attempt in range(3): try: with connection.begin(): debit(connection, from_id, amount) credit(connection, to_id, amount) break except OperationalError as e: if e.orig.args[0] != 1213 or attempt == 2: raise time.sleep(0.05 * 2 ** attempt)Include whatever you read inside the transaction, as those values could have changed. And don’t send emails or call other services from inside it, or they’d repeat on each attempt.
3. Lock rows in the same order
Opposite orders cause cycles, so one consistent order removes them. In the example, sort by id:
first, second = sorted([from_id, to_id])For updates of many rows, one statement doesn’t let you choose the order easily, so lock them first in a known order:
4. Add the missing index
InnoDB locks the rows it scans, not only those it changes. With no useful index, an UPDATE may lock a large part of the table, and then two unrelated updates collide. Check how the query runs:
type: ALL or a huge rows figure is a warning. An index fixes both the speed and the locks:
5. Keep transactions short
The longer a transaction runs, the more it collides. So don’t hold one open while calling an API, waiting for a user, or processing a file. Break a large update into batches, with a commit for each:
Repeat until it affects no rows.
6. Gap locks and INSERT
Under the default REPEATABLE READ, InnoDB also locks the gaps between rows. That’s behind a typical surprise: two sessions running SELECT ... FOR UPDATE on a row that doesn’t exist, and then both inserting:
SELECT * FROM stock WHERE sku = 'x' FOR UPDATE; -- no row: takes a gap lock, and both can hold itINSERT INTO stock (sku, qty) VALUES ('x', 1); -- waits for the other: deadlockUse one atomic statement instead:
Some workloads do better with READ COMMITTED, which takes far fewer gap locks. It changes what your reads see, so test it first:
Warning
Don’t set innodb_deadlock_detect = OFF to make the error disappear. Your transactions then wait until innodb_lock_wait_timeout, 50 seconds by default, and fail anyway. It’s only for very specific high-concurrency setups, and it makes the common case much worse.
Shell, we have a problem!
Similar, but different: ERROR 1205: Lock wait timeout exceeded is no cycle: one session held a lock too long, and only the statement fails unless innodb_rollback_on_timeout is set. ERROR 1040 is about too many connections. And PostgreSQL gives the same situation as deadlock detected, where the same advice applies.
Found this useful? Share it.
Found a mistake? Let us know at contact@ohsudo.com.