Forcing deadlock rollback victim transaction

If you use a storage engine that supports transactions, you probably have faced or heard of deadlock's.

From MySQL Documentation:
“Always be prepared to re-issue a transaction if it fails due to deadlock. Deadlocks are not dangerous. Just try again.”

At work, we had an important job that sometimes were failing due to dead lock. I wanted to enhance it, so it will do what the documentation says ( Retry the transaction ). In order to do that, I wanted to have a scenario where I was able to reproduce the deadlock and the victim transaction was the one from the job I was fixing.

Create a deadlock is simple, you just need to have 2 sessions that each one holds a lock that the other is waiting for. For example:
We have a table that has 4 entries on it (entry 1, entry 2, entry 3, entry 4) and we have 2 transactions that do the follow in time order:

T1: locks entry 1;
T2: locks entry 4;
T1: require a lock on entry 4 (it will wait until T2 release the lock);
T2: require a lock on entry 1 (it will wait until T1 release the lock);

At this point each transaction will wait for each other and MySQL detects the deadlock. Let’s see it in practice:

T1 > CREATE TABLE t (i INT, PRIMARY KEY(i)) ENGINE = InnoDB;
Query OK, 0 rows affected (0.08 sec)

T1 > INSERT INTO t VALUES (1),(2),(3),(4);
Query OK, 4 rows affected (0.00 sec)
Records: 4  Duplicates: 0  Warnings: 0

T1 > START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

T1 > UPDATE t SET i = 1 WHERE i = 1;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T2 > START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

T2 > UPDATE t SET i = 4 WHERE i = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T1 >  UPDATE t SET i = 4 WHERE i = 4;
Query OK, 0 rows affected (9.54 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T2 > UPDATE t SET i = 1 WHERE i = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

How MySQL choose the victim transaction to be rolled back ?

MySQL does an internal calculation based on transaction weight. It calculates it based on the amount of rows altered and the number of rows locked on the transaction.
This value can be retrieved on trx_weight column from information_schema.innodb_trx table:

T1 > SELECT * FROM  information_schema.innodb_trx\G
*************************** 1. row ***************************
                    trx_id: 23326
                 trx_state: RUNNING
               trx_started: 2016-02-19 10:10:45
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 5
                 trx_query: SELECT * FROM  information_schema.innodb_trx
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 1
          trx_lock_structs: 3
     trx_lock_memory_bytes: 1136
           trx_rows_locked: 2
         trx_rows_modified: 0
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
1 row in set (0.00 sec)

The tip:
One important factor on the above calculation, is if any transaction have made changes to non-transactional tables, it is considered heavier than the ones that haven’t.
So for my test, I’ve create an MyISAM table and I’ve inserted a row on this table to make it “heavier”:

T2 > CREATE TABLE t2 (i INT, PRIMARY KEY(i)) ENGINE = MyISAM;
Query OK, 0 rows affected (0.02 sec)

T1 > START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

T1 > UPDATE t SET i = 1 WHERE i = 1;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T2 > START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

T2 > UPDATE t SET i = 4 WHERE i = 4;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T2 > INSERT INTO t2 VALUES (1);
Query OK, 1 row affected (0.00 sec)

T1 > UPDATE t SET i = 4 WHERE i = 4;  -- it will wait for T2 lock be released ...

T2 > UPDATE t SET i = 1 WHERE i = 1;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

T1 >
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

As you can see, the victim from rollback was T1.

That is it for today guys. I hope you enjoy it.

Liked? Help and Share!
This entry was posted in InnoDB, MySQL. Bookmark the permalink.
  • Justin Swanhart

    no… please, just no…

    • Marcelo Altmann

      Hi Justin.
      First of all. Thanks for commenting.
      If you read the context of the post, you will see that I’m not suggesting people to use this trick to not have deadlocks. It was useful to provoke deadlock while testing a solution to cope with it. As I think it may help others I decided to share it.
      If you have a better way to do it. Please share it .

      • Justin Swanhart

        Mixing transactional and non-transactional changes in a transaction is abhorrent IMHO. That is why I said no.

      • Justin Swanhart

        I can see the value of this for testing, but I can also see people using it in regular code, making some certain thread heavier on a programmatic basis because they don’t want the RDBMS to choose the victim.