MySQL Deadlock with an an insert that raises a trigger

北城余情 提交于 2020-07-06 04:35:32

问题


I found out some rare deadlock errors happening. I understand deadlock happens when two queries job depends on the result of each other so MySQL rollbacks one of them. But in my situation MySQL is in auto commit mode and I am inserting a new record which fires a trigger. So I don't get the reason it produces a dead lock situation.

Here is my tables schema:

---- users Table ----

CREATE TABLE `users` (
`insta_id` bigint(20) unsigned NOT NULL,
`name` varchar(50) NOT NULL,
`password` varchar(60) NOT NULL,
`gem` int(10) unsigned DEFAULT '20',
`coin` int(10) unsigned DEFAULT '20',
 PRIMARY KEY (`insta_id`),
 UNIQUE KEY `insta_id` (`insta_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

---like_requests Table---

CREATE TABLE `like_requests` (
`req_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`insta_id` bigint(20) unsigned NOT NULL,
`media_id` varchar(50) NOT NULL,
`remaining_like` int(10) unsigned NOT NULL,
`active` tinyint(1) NOT NULL DEFAULT '1',
`count` int(10) unsigned NOT NULL,
PRIMARY KEY (`req_id`),
KEY `insta_id` (`insta_id`),
KEY `media_id` (`media_id`),
CONSTRAINT `like_requests_ibfk_1` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=103902 DEFAULT CHARSET=latin1

---likes Table---

CREATE TABLE `likes` (
 `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
 `insta_id` bigint(20) unsigned NOT NULL,
 `media_id` varchar(50) NOT NULL,
 `req_id` bigint(20) unsigned DEFAULT NULL,
 `date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
 PRIMARY KEY (`id`),
 UNIQUE KEY `id` (`id`),
 KEY `req_id` (`req_id`),
 KEY `insta_id` (`insta_id`),
 KEY `media_id` (`media_id`),
 CONSTRAINT `likes_ibfk_1` FOREIGN KEY (`req_id`) REFERENCES  `like_requests`(`req_id`),
 CONSTRAINT `likes_ibfk_2` FOREIGN KEY (`insta_id`) REFERENCES `users`(`insta_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1704209 DEFAULT CHARSET=latin1

I have a Trigger on likes table, defined as following:

CREATE TRIGGER `after_insert_likes` AFTER INSERT ON `likes`
    FOR EACH ROW BEGIN
        UPDATE users SET users.coin=users.coin+1
            WHERE users.insta_id = NEW.insta_id  LIMIT 1;
        IF NEW.req_id IS NOT NULL THEN
            UPDATE like_requests
                SET  like_requests.remaining_like = like_requests.remaining_like-1
                WHERE like_requests.req_id = NEW.req_id
                  AND like_requests.remaining_like > 0
                LIMIT 1;
        END IF;
    END

With doing some simple inserts:

    $sql = "INSERT INTO likes (insta_id,media_id,req_id) VALUES (?,?,?);";
    $pdo = $this->db;

    $statement = $pdo->prepare($sql);
    $statement->bindValue(1,$data['id'],PDO::PARAM_INT);
    $statement->bindValue(2,$data['media_id']);
    $statement->bindValue(3,$data['req_id'],PDO::PARAM_INT);

    try
    {
        $statement->execute();
        return GetOkResponseWithMessage($response,"Like was submitted");
    }
    catch (PDOException $exc)
    {
        return GetErrorResponseWithMessage($response,$exc->getMessage(),500);
    }

I get the following dead lock error log:

*** (1) TRANSACTION:
TRANSACTION 29031910, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
LOCK WAIT 7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264238, OS thread handle 0x7f6522c6eb00, query id 753506        localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id   LIMIT 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table  `insta_star`.`users` trx table locks 4 total table locks 4  trx id 29031910  lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant  0
*** (2) TRANSACTION:
TRANSACTION 29031909, ACTIVE 1 sec starting index read
mysql tables in use 4, locked 4
7 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 264237, OS thread handle 0x7f65209f8b00, query id 753507 localhost xxxx updating
UPDATE users SET users.coin=users.coin+1 WHERE users.insta_id=NEW.insta_id   LIMIT 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table  `insta_star`.`users` trx table locks 4 total table locks 4  trx id 29031909 lock   mode S locks rec but not gap lock hold time 0 wait time before grant 0
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 14 page no 1560 n bits 128 index `PRIMARY` of table    `insta_star`.`users` trx table locks 4 total table locks 4  trx id 29031909   lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant   0
*** WE ROLL BACK TRANSACTION (2)

Shouldn't this end up in lock wait instead of a dead lock ?

How can I overcome this issue without restarting transaction ?


回答1:


In likes, is (insta_id, media_id) unique? Or maybe (insta_id, req_id)?? Or maybe all 3??? If so, make it the PRIMARY KEY and get rid of id all together. If you must keepid, get rid ofUNIQUE(id), sincePRIMARY KEY(id)` provides that functionality.

Similarly, get rid of UNIQUE(insta_id).

Think of autocommit and TRIGGER combining to be a transaction composed of several commands:

BEGIN;
INSERT INTO likes...  -- Includes 2 uniqueness checks, 1 FK check
UPDATE users ...
if... UPDATE like_requests ...
COMMIT;

My suggested index changes may speed things up some, thereby decreasing the chance of a deadlock. The changes may even turn the deadlock into a wait, but I doubt it.

Your best defense against deadlocks is live with them, and to catch them and replay the transaction (in this case the one INSERT).

(Unrelated:) media_id seems to be stored redundantly.




回答2:


Thread 2 holds a shared lock on the row in the users table.

Then thread 1 tries to obtain an exclusive lock on the same row, and goes into lock wait.

But thread 1 won't have an opportunity to time out, because thread 2 then tries to escalate his lock to exclusive... but to do that, he must wait for thread 1, which is in lock wait, but it is waiting for thread 2.

They are each blocking the other.

That's a deadlock.

The server selects a transaction to kill, so that they don't block each other needlessly.

Deadlock detection allows one thread to immediately succeed at the expense of the other one. Otherwise they'd both be stuck in lock wait until one of them died from waiting too long.


You're in autocommit mode, but of course, that doesn't mean you're not in a transaction. Every query with InnoDB is still handled in a transaction, but with autocommit, the transaction is implicitly started when the query starts to execute and implicitly committed when it succeeds.



来源:https://stackoverflow.com/questions/40618799/mysql-deadlock-with-an-an-insert-that-raises-a-trigger

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!