Deadlock в триггере MySQL с вложенным запросом выбора - PullRequest
0 голосов
/ 04 января 2019

У меня проблемы с BEFORE INSERT TRIGGER, который вызывает взаимоблокировки при одновременной вставке 2 или более server_seed_use .

Я хочу избавиться от взаимоблокировок (если это возможно), поэтому мне не нужно обрабатывать их на уровне приложений. server_seed_use Таблица будет содержать буквально миллионы, если не миллиарды записей. Вставки должны быть производительными, и uniqueIndex(serverSeedId, nonce) необходимо, чтобы один server_seed никогда не имел несколько server_seed_use с одинаковыми nonce.

Частичная схема БД, связанная с этой проблемой:

CREATE TABLE `server_seed` (
    `id` INT NOT NULL AUTO_INCREMENT
    ,`seed` CHAR(64) COLLATE \ "utf8_general_ci\" NOT NULL
    ,`hash` CHAR(64) COLLATE \ "utf8_general_ci\" NOT NULL
    ,`userId` INT NULL
    ,UNIQUE INDEX `IDX_caf256dbf86619a07597158267`(`seed`)
    ,UNIQUE INDEX `IDX_ea5ad02726433cb3e6969e65e3`(`hash`)
    ,PRIMARY KEY (`id`)
    ) ENGINE = InnoDB;

CREATE TABLE `server_seed_use` (
    `id` INT NOT NULL AUTO_INCREMENT
    ,`clientSeed` VARCHAR(64) NOT NULL
    ,`nonce` INT NULL DEFAULT NULL
    ,`serverSeedId` INT NULL
    ,INDEX `IDX_3b9f114a6190aae9cf7b0aeaab`(`nonce`)
    ,UNIQUE INDEX `IDX_2b872fdbfb8da47ca2744c53a8`(`serverSeedId`, `nonce`)
    ,PRIMARY KEY (`id`)
    ) ENGINE = InnoDB;

Определение триггера:

CREATE TRIGGER test_trigger
    BEFORE INSERT ON server_seed_use
    FOR EACH ROW BEGIN
        DECLARE nextNonce INT;

        SET nextNonce = (SELECT MAX(nonce) FROM server_seed_use WHERE serverSeedId=NEW.serverSeedId); <--- I am convinced this is causing deadlocks
        IF (nextNonce IS NULL) THEN
            SET nextNonce = -1;
        END IF;

        SET NEW.nonce=nextNonce + 1;
    END

Предполагается обновить nonce (установленный при вставке) следующим образом:

user_1(id: 1)
    server_seed_1(id: 1, owner: 1)
        server_seed_use_1(id: 1, serverSeedId: 1, nonce: 0)
        server_seed_use_2(id: 2, serverSeedId: 1, nonce: 1)
        server_seed_use_5(id: 5, serverSeedId: 1, nonce: 2)
    server_seed_2(id: 2, owner: 1)
        server_seed_use_3(id: 3, serverSeedId: 2, nonce: 0)
        server_seed_use_4(id: 4, serverSeedId: 2, nonce: 1)
user_2(id: 2)
    server_seed_3(id: 3, owner: 2)
        server_seed_use_6(id: 6, serverSeedId: 3, nonce: 0)
        server_seed_use_7(id: 7, serverSeedId: 3, nonce: 1)

Запрос, который вызывает тупик, выглядит следующим образом:

INSERT INTO `server_seed_use`(`id`, `clientSeed`, `nonce`, `serverSeedId`) VALUES (DEFAULT, "clientSeed", DEFAULT, 1)

Если выполняется только один раз или после того, как предыдущий запрос завершен, работает просто отлично, но после одновременного выполнения 2 или более раз возникает тупик.

Я протестировал различные механизмы блокировки чтения для запроса выбора (SELECT MAX(nonce) FROM server_seed_use WHERE serverSeedId=NEW.serverSeedId):

  1. без или LOCK IN SHARE MODE:

    ER_LOCK_DEADLOCK: Deadlock found when trying to get lock; try restarting transaction.

  2. FOR UPDATE

    ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG: Can't update table 'server_seed_use' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.

Выход SHOW ENGINE INNODB STATUS после возникновения тупика:

| InnoDB |      |
=====================================
2019-01-04 15:01:26 0x7fa1a642d700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 10 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1733 srv_active, 0 srv_shutdown, 67024 srv_idle
srv_master_thread log flush and writes: 68757
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 130200
OS WAIT ARRAY INFO: signal count 81336
RW-shared spins 0, rounds 203699, OS waits 112137
RW-excl spins 0, rounds 145754, OS waits 1538
RW-sx spins 11622, rounds 72192, OS waits 223
Spin rounds per wait: 203699.00 RW-shared, 145754.00 RW-excl, 6.21 RW-sx
------------------------
LATEST FOREIGN KEY ERROR
------------------------
2019-01-04 02:23:41 0x7fa1bf907700 Transaction:
TRANSACTION 1909787, ACTIVE 0 sec inserting
mysql tables in use 2, locked 2
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 608, OS thread handle 140332680378112, query id 36689 172.17.0.10 test
INSERT INTO `server_seed_use`(`id`, `clientSeed`, `nonce`, `serverSeedId`) VALUES (DEFAULT, 'aaa', DEFAULT, 1)
Foreign key constraint fails for table `test`.`server_seed_use`:
,
CONSTRAINT `FK_317cca594917646e8d2667d8317` FOREIGN KEY (`serverSeedId`) REFERENCES `server_seed` (`id`)
Trying to add in child table, in index IDX_2b872fdbfb8da47ca2744c53a8 tuple:
DATA TUPLE: 3 fields;
0: len 4; hex 80000000; asc     ;;
1: SQL NULL;
2: len 4; hex 80000001; asc     ;;

But in parent table `test`.`server_seed`, in index PRIMARY,
the closest match we can find is record:
PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 0000001d2419; asc \$ ;;
2: len 7; hex bf0000019c0110; asc ;;
3: len 30; hex 326431623961656431633066316431396630383030363032353037646261; asc 2d1b9aed1c0f1d19f0800602507dba; (total 64 bytes);
4: len 30; hex 376635626535643763393761373036613635663331316531643561333334; asc 7f5be5d7c97a706a65f311e1d5a334; (total 64 bytes);
5: len 4; hex 80000001; asc ;;

---

## LATEST DETECTED DEADLOCK

2019-01-04 14:45:33 0x7fa1a67fc700
**_ (1) TRANSACTION:
TRANSACTION 1980933, ACTIVE 0 sec inserting
mysql tables in use 2, locked 2
LOCK WAIT 6 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 1114, OS thread handle 140332419888896, query id 153107 172.17.0.10 test
INSERT INTO `server_seed_use`(`id`, `clientSeed`, `nonce`, `serverSeedId`) VALUES (DEFAULT, "clientSeed", DEFAULT, 1)
_** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 21479 page no 4 n bits 72 index IDX_2b872fdbfb8da47ca2744c53a8 of table `test`.`server_seed_use` trx id 1980933 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

**_ (2) TRANSACTION:
TRANSACTION 1980930, ACTIVE 0 sec inserting
mysql tables in use 2, locked 2
7 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 1
MySQL thread id 1113, OS thread handle 140332259854080, query id 153101 172.17.0.10 test
INSERT INTO `server_seed_use`(`id`, `clientSeed`, `nonce`, `serverSeedId`) VALUES (DEFAULT, "clientSeed", DEFAULT, 1)
_** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 21479 page no 4 n bits 72 index IDX_2b872fdbfb8da47ca2744c53a8 of table `test`.`server_seed_use` trx id 1980930 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 4; hex 80000000; asc ;;
2: len 4; hex 80000001; asc ;;

\*\*\* (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 21479 page no 4 n bits 72 index IDX_2b872fdbfb8da47ca2744c53a8 of table `test`.`server_seed_use` trx id 1980930 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

## \*\*\* WE ROLL BACK TRANSACTION (1)

## TRANSACTIONS

Trx id counter 1980956
Purge done for trx's n:o < 1980956 undo n:o < 0 state: running but idle
History list length 479
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421808177219408, not started
0 lock struct(s), heap size 1136, 0 row lock(s)

---

## FILE I/O

I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (write thread)
I/O thread 7 state: waiting for completed aio requests (write thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
ibuf aio reads:, log i/o's:, sync i/o's:
Pending flushes (fsync) log: 0; buffer pool: 0
3815 OS file reads, 418691 OS file writes, 261409 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s

---

## INSERT BUFFER AND ADAPTIVE HASH INDEX

Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
insert 0, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 2 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
Hash table size 34679, node heap has 1 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s

---

## LOG

Log sequence number 6612985217
Log flushed up to 6612985217
Pages flushed up to 6612985217
Last checkpoint at 6612985208
0 pending log flushes, 0 pending chkp writes
165104 log i/o's done, 0.00 log i/o's/second

---

## BUFFER POOL AND MEMORY

Total large memory allocated 137428992
Dictionary memory allocated 855236
Buffer pool size 8192
Free buffers 1024
Database pages 7159
Old database pages 2622
Modified db pages 0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 17329, not young 362652
0.00 youngs/s, 0.00 non-youngs/s
Pages read 3754, created 155998, written 162468
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 7159, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]

---

## ROW OPERATIONS

0 queries inside InnoDB, 0 queries in queue
0 read views open inside InnoDB
Process ID=1, Main thread ID=140332486358784, state: sleeping
Number of rows inserted 5343638, updated 573, deleted 0, read 5746447
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s

---

# END OF INNODB MONITOR OUTPUT



@ Edit1 : Я не смог уместить мой ответ на @ krokodilko в комментарии, поэтому я пишу его здесь.

Я бы предпочел избежать решения 2 + 3 , поскольку оно создает еще больше проблем.

Опция 1 + 4 выглядит интересно. Я уже добавил ER_LOCK_DEADLOCK check в "критической" части приложения + логика повторов. Таким образом, добавив 1 , моя проблема была частично решена.

Однако, много ER_LOCK_DEADLOCK происходит, что означает, что должно быть много попыток (я полагаю, не очень эффективно). Я хотел бы хотя бы попытаться уменьшить количество взаимоблокировок, если невозможно полностью избавиться от них.

Что касается запроса в 4 Я думаю, вы имели в виду

SELECT id INTO some_variable FROM server_seed_<strong>use</strong> WHERE <strong>serverSeedId</strong> = NEW.serverSeedId FOR UPDATE;
, так как я вставляю server_seed_use вместо server_seed. Поправь меня, если я ошибаюсь.

Я не эксперт по MySQL, но думаю, что этой блокировки будет недостаточно. Он защитит все существующие server_seed_use с определенными serverSeedId, но не защитит от вставки новых server_seed_use с serverSeedId. Имеет ли это смысл? Если это так, возможно ли заблокировать все вставки или на основе serverSeedId, поэтому в каждый момент времени может быть только одна server_seed_use с определенной serverSeedId вставкой.

1 Ответ

0 голосов
/ 05 января 2019

Это не ответ на вопрос (или - это только частичный ответ).
Просто приведенный ниже текст слишком длинный, чтобы поместиться в комментарии, поэтому я даю его в качестве ответа на вопрос


Какой ORM? Hibernate, MyBatis, Toplink ...?

В любом случае, эта взаимоблокировка, скорее всего, связана с тем, как MySql обрабатывает Разрыв индекса InnoDB , хотя для обеспечения уверенности на 100% придется отладить код (придется потратить больше времени на эксперименты) ,

У вас есть несколько вариантов:

  1. перехватить исключение тупиковой ситуации, выполнить откат и повторить всю транзакцию.
  2. изменить уровень изоляции с повторяемого чтения (по умолчанию) на зафиксированное чтение.
  3. включить системную переменную innodb_locks_unsafe_for_binlog (не рекомендуется, поскольку она устарела - см. документацию )
  4. используйте дополнительный SELECT FOR UPDATE, чтобы заблокировать некоторые записи, чтобы сериализовать все транзакции (вероятно, лучшим будет SELECT * FROM server_seed WHERE id = NEW.serverSeedId FOR UPDATE).

Опции 2 + 3, вероятно, исправят проблему взаимоблокировки, но, скорее всего, они не будут удовлетворять, потому что вместо этого вы получите «ошибку дублирующегося ключа» - это связано с ошибкой в ​​логике триггера (я оставлю это сейчас, потому что это потребовало бы более длинного объяснения).

1 + 4 варианта остается лучшим.

Вариант 1 очевиден - должен быть реализован где-то в коде.

Вариант 4 - Вы можете попробовать добавить SELECT id INTO some_variable FROM server_seed WHERE id = NEW.serverSeedId FOR UPDATE в качестве первой инструкции в триггере (при условии, что в этой таблице есть запись с этим идентификатором server_seed). Или попробуйте добавить его в начале транзакции - я просто не знаю, как это сделать в вашем ORM.

...