Skip to main content

How to Avoid Deadlocks in MySQL by Locking Rows in the Same Order

· 6 min read
Jagdish Kumawat
Founder @ Dewiride

A deadlock needs a cycle: I hold what you want, and you hold what I want. If everyone takes rows in the same order, the cycle can't form, and the second transaction waits instead of failing. This is the most effective fix for deadlocks, and it's a small change.

Prerequisites​

Opposite Orders: A Deadlock​

Each transaction changes rows 3 and 4, in a different order:

mysql: terminal 1
START TRANSACTION;
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 3;
mysql: terminal 2
START TRANSACTION;
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 4;
mysql: terminal 1
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 4;

That waits for terminal 2. Then terminal 2 asks for what terminal 1 holds:

mysql: terminal 2
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 3;
Output
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

One of them lost all its work, and has to start over.

The Same Order: A Short Wait​

Now both take row 3 first:

mysql: terminal 1
START TRANSACTION;
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 3;
mysql: terminal 2
START TRANSACTION;
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 3;

Terminal 2 waits, holding nothing, so terminal 1 can finish without running into it:

mysql: terminal 1
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 4;
COMMIT;

Terminal 2 gets its row, and carries on:

mysql: terminal 2
UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 4;
COMMIT;
SELECT Id, QuantityOnHand FROM products WHERE Id IN (3, 4);
Output (terminal 2, from the first UPDATE)
mysql> UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 3;
Query OK, 1 row affected (2.141 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> UPDATE products SET QuantityOnHand = QuantityOnHand - 1 WHERE Id = 4;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> COMMIT;
Query OK, 0 rows affected (0.004 sec)

mysql> SELECT Id, QuantityOnHand FROM products WHERE Id IN (3, 4);
+----+----------------+
| Id | QuantityOnHand |
+----+----------------+
| 3 | 48 |
| 4 | 48 |
+----+----------------+
2 rows in set (0.001 sec)

Terminal 2's first UPDATE taking 2.141 seconds while it waited its turn, then the second UPDATE and COMMIT succeeding, and a SELECT showing both rows at 48.

It took 2.1 seconds instead of an error, and both changes count: from 50 to 48.

MySQL's deadlock handling page puts it as: "When modifying multiple tables within a transaction, or different sets of rows in the same table, do those operations in a consistent order each time."

How to Pick an Order​

You're changingA simple rule
Several rows in one tableLowest primary key first. Sort them in code
Several tablesThe same table order everywhere
A parent and a child rowThe parent first

In .NET, that's one OrderBy before the loop: How to Prevent Overselling with a Conditional UPDATE in EF Core sorts its lines. In a test here, 30 simultaneous orders in opposite orders gave 15 deadlocks each round before sorting, and none after.

Other Habits That Help​

From the same page:

  • Keep transactions short. "Keep transactions small and short in duration to make them less prone to collision." Why You Should Not Do Slow Work Inside a Database Transaction shows the cost.
  • Use an index on what you filter by, so a statement locks fewer rows. Without one, it locks them all.
  • Be ready to retry. "Always be prepared to re-issue a transaction if it fails due to deadlock." Lock order makes them rare, and not impossible.

Common Mistakes​

MistakeWhat happensDo this instead
Processing a list in the order a client sent itClients decide your lock orderSort it
Fixing the order in one code path onlyThe other path still collidesUse one rule everywhere
Treating a retry as the whole fixDeadlocks keep costing workFix the order, and keep the retry

Troubleshooting​

SymptomLikely causeFix
Deadlocks on the same rows, in the same orderAnother lock is involved, such as a shared lock from a foreign keyRead the report
Deadlocks only under loadTwo paths use different ordersCompare the two statements in the report
Long waits, but no deadlocksThe order's right, and a transaction is slowShorten it

FAQ​

Does it slow things down?​

Waiting a moment costs far less than rolling back and redoing the work. Here, the second transaction waited for as long as the first one needed.

Conclusion​

Deadlocks need two transactions taking rows in opposite orders. Sort your rows and use one table order, and the second transaction waits instead of failing. Keep transactions short, and keep a retry for the rare one that gets through.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.