skip to Main Content

In an attempt to overcome deadlocks on the combat server, I reached a dead end. There are 2 tables:

First:

create table table_1
(
    id    int auto_increment
        primary key,
    data1 text null,
    data2 text null
);

Second:

create table table_2
(
    id    int auto_increment
        primary key,
    t1_id int  null,
    data1 text null,
    data2 text null,
    constraint table_2_table_1_id_fk
        foreign key (t1_id) references table_1 (id)
            on update cascade on delete cascade
);

For tests, there are 30 records in table_1, 60 in table_2 (every 2 records from table_2 refer by key to 1 record in table_1).

Next, a simple php script that updates some records from table_1, by condition from table_2 in an infinite loop:

<?php
$db = new PDO("mysql:host=127.0.0.1;dbname=test_db;", 'debian-sys-maint', 'pass', [
    PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8'",
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$random = rand(0, 9);
while (true) {
    $db->exec("
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;");
}

So, I run this PHP script in 150 instances, and a Deadlock error is generated. I tried to fix it by modifying the query as follows:

UPDATE table_1
    INNER JOIN (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id) table_2 on table_1.id = table_2.t1_id
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;
WITH tmp (id) AS (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id)
UPDATE table_1
    INNER JOIN tmp on table_1.id = tmp.id
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;
UPDATE table_1
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1
WHERE table_1.id in (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id);
UPDATE table_1, (SELECT t1_id FROM table_2 WHERE table_2.data2 like '%$random%' ORDER BY t1_id) tmp1
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1
WHERE table_1.id = tmp1.t1_id;

Actually the first question: I don’t understand where the deadlock comes from, if the records are always sorted in the same order, the race condition is excluded. This is what the SHOW ENGINE INNODB STATUS; returns when catching a deadlock:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-05-10 01:56:01 140233769219840
*** (1) TRANSACTION:
TRANSACTION 3529589, ACTIVE 1 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 7 lock struct(s), heap size 1128, 90 row lock(s)
MySQL thread id 20, OS thread handle 140233708357376, query id 173 localhost 127.0.0.1 debian-sys-maint executing
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%9%'
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529589 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 224 index PRIMARY of table `test_db`.`table_1` trx id 3529589 lock_mode X locks rec but not gap waiting
.....................
*** (2) TRANSACTION:
TRANSACTION 3529887, ACTIVE 0 sec starting index read
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1128, 52 row lock(s)
MySQL thread id 118, OS thread handle 140229428811520, query id 444 localhost 127.0.0.1 debian-sys-maint executing
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%7%'
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529887 lock_mode X locks rec but not gap
.....................
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2 page no 4 n bits 224 index PRIMARY of table `test_db`.`table_1` trx id 3529887 lock_mode X locks rec but not gap waiting
.....................
*** WE ROLL BACK TRANSACTION (2)

The only way I managed to solve the deadlock was to create a temporary table that would contain a ready-made set of keys that would be affected by UPDATE immediately from table_1:

DROP TABLE IF EXISTS tmp1;
CREATE TEMPORARY TABLE tmp1
SELECT table_2.t1_id FROM table_2 WHERE table_2.data2 like '%$random%'

UPDATE table_1
    INNER JOIN tmp1 on table_1.id = tmp1.t1_id
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;

Or with the same success I can create a copy of table_2 in a temporary table, it also does not cause deadlocks:

DROP TABLE IF EXISTS tmp1;
CREATE TEMPORARY TABLE tmp1
SELECT * FROM table_2 WHERE 1;

UPDATE table_1
    INNER JOIN tmp1 on table_1.id = tmp1.t1_id and tmp1.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;

The second question is: Why does this option not cause deadlocks? As I understand it, it is the records from table_1 that are blocked, in this case they are blocked in the same way, only the search does not take place according to table_2, but according to the temporary table tmp1.

It would seem that my problem is solved, deadlocks are not called, but I did not like the solution with a temporary table and I continued my tests during which I came across a very strange thing that finally drove me to a dead end. If you start and complete the transaction yourself, then deadlocks do not appear:

BEGIN;
UPDATE table_1
INNER JOIN table_2 on table_1.id = table_2.t1_id and table_2.data2 like '%$random%'
SET table_1.data1 = table_1.data1 + 1,
    table_1.data2 = table_1.data2 + 1;
COMMIT;

This is probably the last and most exciting question for me… I have an autocommit enabled, why if I don’t explicitly start and complete the transaction, then deadlocks come out? PHP is known to work in one thread, for 1 script there is exactly 1 connection to the database, all 150 scripts work in parallel

2

Answers


  1. I think this is because of the isolation level. The default is REPEATABLE READ and the second clause maybe the problem.

    • For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it.

    • For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key locks to block insertions by other sessions into the gaps covered by the range. For information about gap locks and next-key locks, see Section 15.7.1, “InnoDB Locking”.

    From dev.mysql

    So, try SERIALIZABLE

    This level is like REPEATABLE READ, but InnoDB implicitly converts all plain SELECT statements to SELECT … FOR SHARE if autocommit is disabled. If autocommit is enabled, the SELECT is its own transaction. It therefore is known to be read only and can be serialized if performed as a consistent (nonlocking) read and need not block for other transactions. (To force a plain SELECT to block if other transactions have modified the selected rows, disable autocommit.)

    Login or Signup to reply.
  2. I think it is simply that

    table_2.data2 like '%$random%'
    

    has a leading wildcard, thereby necessitating a full table scan. In doing so, it either locks too many rows, or bumps into the other thread.

    Note: "90 row lock(s)"

    Can the LIKE be improved and provide a suitable INDEX? Perhaps FULLTEXT would be useful here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search