Tại sao truy vấn này dẫn đến bế tắc?


7

Tôi cung cấp bên dưới truy vấn MySQL thô và mã mà tôi thực hiện theo chương trình. Nếu hai yêu cầu đang được thực hiện cùng một lúc sẽ dẫn đến mẫu lỗi sau:

SQLSTATE [40001]: Lỗi nối tiếp: 1213 Bế tắc được tìm thấy khi cố gắng khóa; thử khởi động lại giao dịch (SQL update user_chats set updated_at = 2018-06-29 10:07:13 where id = 1:)

Nếu tôi thực hiện cùng một truy vấn nhưng không có khối giao dịch xung quanh thì nó sẽ hoạt động không có lỗi với nhiều cuộc gọi đồng thời. Tại sao ? (Khóa giao dịch khóa, phải không?)

Có cách nào để giải quyết điều này mà không khóa toàn bộ bảng không? (Muốn thử tránh khóa cấp bảng)

Tôi biết rằng khóa được lấy để chèn / cập nhật / xóa bảng trong MySql bằng InnoDB nhưng vẫn không hiểu tại sao bế tắc xảy ra ở đây và cách giải quyết nó theo cách hiệu quả nhất.

    START TRANSACTION;

    insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`)
        values (1, 2, 'dfasfdfk);
    update `user_chats`
        set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

    COMMIT;

Trên đây là truy vấn thô, nhưng tôi thực hiện nó trong PHP Trình tạo truy vấn Laravel như sau:

    /**
     * @param UserChatMessageEntity $message
     * @return int
     * @throws \Exception
     */
    public function insertChatMessage(UserChatMessageEntity $message) : int
    {
        $this->db->beginTransaction();
        try
        {
            $id = $this->db->table('user_chat_messages')->insertGetId([
                    'user_chat_id' => $message->getUserChatId(),
                    'from_user_id' => $message->getFromUserId(),
                    'content' => $message->getContent()
                ]
            );

            //TODO results in lock error if many messages are sent same time
            $this->db->table('user_chats')
                ->where('id', $message->getUserChatId())
                ->update(['updated_at' => date('Y-m-d H:i:s')]);

            $this->db->commit();
            return $id;
        }
        catch (\Exception $e)
        {
            $this->db->rollBack();
            throw  $e;
        }
    }

DDL cho các bảng:

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    CONSTRAINT user_chat_messages_user_chat_id_foreign FOREIGN KEY (user_chat_id) REFERENCES user_chats (id),
    CONSTRAINT user_chat_messages_from_user_id_foreign FOREIGN KEY (from_user_id) REFERENCES users (id)
);
CREATE INDEX user_chat_messages_from_user_id_index ON user_chat_messages (from_user_id);
CREATE INDEX user_chat_messages_user_chat_id_index ON user_chat_messages (user_chat_id);


CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

Tính như thế nào chat_user_messages.Id? Bạn có thể đăng DDL cho các bảng không?
Michael Kutz

Đây là trường tăng tự động. id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENTTôi đã thực hiện một thử nghiệm và nếu tôi loại bỏ giao dịch thì bế tắc sẽ không xảy ra nữa. Nhưng dù sao tôi cũng cần thực hiện theo cách giao dịch, thông báo chỉ được chèn nếu người dùng cũng được cập nhật. Tôi đã cập nhật câu hỏi với các ddls cho cả hai bảng có liên quan
Kristi Jorgji

Câu trả lời:


14

KEY NGOẠI TỆ user_chat_messages_user_chat_id_foreignlà nguyên nhân của sự bế tắc của bạn, trong tình huống này.

May mắn thay, điều này rất dễ tái tạo dựa trên thông tin bạn đã cung cấp.

Thiết lập

CREATE DATABASE dba210949;
USE dba210949;

CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    CONSTRAINT user_chat_messages_user_chat_id_foreign FOREIGN KEY (user_chat_id) REFERENCES user_chats (id)
);

insert into user_chats (id,updated_at) values (1,NOW());

Lưu ý rằng tôi đã xóa user_chat_messages_from_user_id_foreignkhóa ngoại vì nó tham chiếu usersbảng mà chúng ta không có trong ví dụ của mình. Nó không quan trọng để tái tạo vấn đề.

Tái sản xuất bế tắc

Kết nối 1

USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');

Kết nối 2

USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');

Kết nối 1

update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

Tại thời điểm này, kết nối 1 đang chờ.

Kết nối 2

update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

Ở đây, Kết nối 2 ném bế tắc

LRI 1213 (40001): Tìm thấy bế tắc khi cố gắng khóa; thử khởi động lại giao dịch

Đang thử lại mà không có khóa ngoại

Hãy lặp lại các bước tương tự, nhưng với các cấu trúc bảng sau. Sự khác biệt duy nhất trong khoảng thời gian này là loại bỏ user_chat_messages_user_chat_id_foreignkhóa ngoại.

CREATE DATABASE dba210949;
USE dba210949;

CREATE TABLE user_chats
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

CREATE TABLE user_chat_messages
(
    id INT(10) unsigned PRIMARY KEY NOT NULL AUTO_INCREMENT,
    user_chat_id INT(10) unsigned NOT NULL,
    from_user_id INT(10) unsigned NOT NULL,
    content VARCHAR(500) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
);

insert into user_chats (id,updated_at) values (1,NOW());

Tái tạo các bước tương tự như trước đây

Kết nối 1

USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');

Kết nối 2

USE dba210949;
START TRANSACTION;
insert into `user_chat_messages` (`user_chat_id`, `from_user_id`, `content`) values (1, 2, 'dfasfdfk');

Kết nối 1

update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

Tại thời điểm này, Kết nối 1 thực thi, thay vì chờ đợi như trước đây.

Kết nối 2

update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1;

Kết nối 2 bây giờ là kết nối đang chờ, nhưng nó chưa bị bế tắc.

Kết nối 1

commit;

Kết nối 2 bây giờ dừng chờ và thực hiện lệnh của nó.

Kết nối 2

commit;

Xong, không có bế tắc.

Tại sao?

Hãy nhìn vào đầu ra của SHOW ENGINE INNODB STATUS

------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-07-04 10:38:31 0x7fad84161700
*** (1) TRANSACTION:
TRANSACTION 42061, ACTIVE 55 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 2, OS thread handle 140383222380288, query id 81 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42061 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** (2) TRANSACTION:
TRANSACTION 42062, ACTIVE 46 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3, OS thread handle 140383222109952, query id 82 localhost root updating
update `user_chats` set `updated_at` = '2018-06-28 08:33:14' where `id` = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3711 page no 3 n bits 72 index PRIMARY of table `dba210949`.`user_chats` trx id 42062 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 00000000a44b; asc      K;;
 2: len 7; hex b90000012d0110; asc     -  ;;
 3: len 4; hex 5b3ca335; asc [< 5;;
 4: len 4; hex 5b3ca335; asc [< 5;;

*** WE ROLL BACK TRANSACTION (2)

Bạn có thể thấy rằng giao dịch 1 có lock_mode X trên phím TIỂU của user_chats, trong khi giao dịch 2 có lock_mode S , và đang chờ lock_mode X . Đó là kết quả của việc nó có được một khóa chia sẻ trước tiên (từ INSERTtuyên bố của chúng tôi ), và sau đó là một khóa độc quyền (từ chúng tôi UPDATE).

Vì vậy, những gì đang xảy ra là Kết nối 1 lấy khóa được chia sẻ trước, và sau đó Kết nối 2 sẽ lấy khóa được chia sẻ trên cùng một bản ghi. Điều đó tốt, bây giờ, vì cả hai đều là khóa chung.

Kết nối 1 sau đó cố gắng nâng cấp lên một khóa độc quyền để thực hiện CẬP NHẬT, chỉ để thấy rằng kết nối 2 đã có khóa. Các khóa được chia sẻ và độc quyền không trộn lẫn với nhau, vì bạn có thể suy ra bằng tên của chúng. Đó là lý do tại sao nó chờ sau UPDATElệnh trên Kết nối 1.

Sau đó, Kết nối 2 cố gắng UPDATE, trong đó yêu cầu khóa độc quyền và InnoDB sẽ "tự sướng, tôi sẽ không bao giờ có thể tự khắc phục tình trạng này" và tuyên bố bế tắc. Nó tắt Kết nối 2, giải phóng khóa chia sẻ mà Kết nối 2 đang giữ và cho phép Kết nối 1 hoàn thành bình thường.

Các giải pháp)

Tại thời điểm này, có lẽ bạn đã sẵn sàng dừng lại với yap yap yap và muốn có một giải pháp. Dưới đây là những gợi ý của tôi, theo thứ tự sở thích cá nhân của tôi.

1. Tránh cập nhật hoàn toàn

Đừng bận tâm với updated_atcột trong user_chatsbảng cả. Thay vào đó, hãy thêm một chỉ mục tổng hợp trên user_chat_messagescho các cột ( user_chat_id, created_at).

ALTER TABLE user_chat_messages
ADD INDEX `latest_message_for_user_chat` (`user_chat_id`,`created_at`)

Sau đó, bạn có thể có được thời gian cập nhật gần đây nhất với truy vấn sau.

SELECT MAX(created_at) AS created_at FROM user_chat_messages WHERE user_chat_id = 1

Truy vấn này sẽ thực hiện cực kỳ nhanh chóng do chỉ mục và không yêu cầu bạn lưu trữ updated_atthời gian mới nhất trong user_chatsbảng. Điều này giúp tránh trùng lặp dữ liệu, đó là lý do tại sao nó là giải pháp ưa thích của tôi.

Hãy chắc chắn để tự động thiết lập idđể các $message->getUserChatId()giá trị, và không khó mã hoá để 1, như trong ví dụ của tôi.

Đây thực chất là những gì Rick James đang gợi ý.

2. Khóa các bảng để tuần tự hóa các yêu cầu

SELECT id FROM user_chats WHERE id=1 FOR UPDATE

Thêm phần này SELECT ... FOR UPDATEvào lúc bắt đầu giao dịch của bạn và nó sẽ tuần tự hóa các yêu cầu của bạn. Như trước đây, hãy chắc chắn để tự động thiết lập idđể các $message->getUserChatId()giá trị, và không khó mã hoá để 1, như trong ví dụ của tôi.

Đây là những gì Gerard H. Pille đang gợi ý.

3. Thả khóa ngoại

Đôi khi, chỉ dễ dàng hơn để loại bỏ nguồn bế tắc. Chỉ cần thả user_chat_messages_user_chat_id_foreignkhóa ngoại, và vấn đề được giải quyết.

Tôi không đặc biệt thích giải pháp này nói chung, vì tôi thích tính toàn vẹn dữ liệu (mà khóa ngoại cung cấp), nhưng đôi khi bạn cần phải đánh đổi.

4. Thử lại lệnh sau khi bế tắc

Đây là giải pháp được khuyến nghị cho các bế tắc nói chung. Chỉ cần bắt lỗi và thử lại toàn bộ yêu cầu. Tuy nhiên, dễ thực hiện nhất nếu bạn chuẩn bị ngay từ đầu và việc cập nhật mã kế thừa có thể khó khăn. Với thực tế là có những giải pháp dễ dàng hơn (như 1 và 2 ở trên) là lý do tại sao đây là giải pháp ít được đề xuất nhất cho tình huống của bạn.


Cảm ơn câu trả lời tuyệt vời! (Tôi không thể upvote không may do thiếu danh tiếng). Id chỉ được mã hóa cứng ở đây trong câu hỏi, nhưng trong mã thực sự của tôi tất nhiên là động. Userchatid được biết trước và được đặt thành đối tượng UserChatMessage. Tôi cũng sẽ cố gắng tự mình tất cả các bước mà bạn cung cấp để tôi hoàn toàn hiểu và tránh trong trường hợp như vậy trong tương lai. Cảm ơn ! Tôi đã giải quyết vấn đề này trước 2 ngày khi Gerard H. Pille gợi ý và đang làm việc rất tốt. WIll cũng thử tất cả các giải pháp của bạn cho mục đích học tập
Kristi Jorgji

Vâng, câu trả lời tuyệt vời. (Tôi đã nâng cấp nó.) Tuy nhiên, một lý do khác tại sao FK có thể gây phiền toái hơn giá trị.
Rick James

@KristiJorgji Nghe hay đấy. Tôi đã đưa ra câu hỏi của bạn một upvote (dù sao nó cũng xứng đáng với tất cả thông tin tốt để tái tạo vấn đề) để có thể cung cấp cho bạn đủ đại diện để upvote và chọn câu trả lời. Vì giải pháp của Gerard đã khắc phục vấn đề của bạn, tôi sẽ đề nghị chấp nhận câu trả lời của anh ấy, nhưng bạn cũng có thể nêu lên câu trả lời của mọi người cũng hữu ích.
Willem Renzema

Câu trả lời tuyệt vời. Tôi đã có vấn đề tương tự. Câu trả lời của bạn đã giúp tôi.
trò chuyện

0

Là bước đầu tiên trong giao dịch của bạn, hãy khóa trên bảng $ this-> db-> ('user_chats') -> where ('id', $ message-> getUserChatId ()). Điều này sẽ tránh được bế tắc.


Vì vậy, trước tiên bạn nên đề xuất một câu lệnh chọn mặc dù tôi không cần kết quả sau đó tiến hành phần còn lại như thực tế? Vui lòng cung cấp cho tôi các chi tiết của giải pháp này một số giải thích để tôi hiểu cách giải quyết vấn đề này và tôi tìm hiểu hình thức của nó
Kristi Jorgji

Bạn có hiểu bế tắc không? Tôi đã đề nghị khóa (trên Oracle đây sẽ là "chọn ... để cập nhật"), để đảm bảo bạn sẽ có thể cập nhật, sẽ không có ai ở giữa (giao dịch được cho là rất ngắn của bạn).
Gerard H. Pille

Đó cũng là cú pháp tương tự trong MySql. Cảm ơn tuyệt vời sẽ làm điều đó sau đó lấy một khóa cho hàng đó
Kristi Jorgji

0

Nếu chỉ có một hàng trong user_chats? Nếu không, ngữ nghĩa của là idgì? Nó có phải là "người dùng" không? Hay một "số trò chuyện"? Hay cái gì khác?

Có vẻ như tất cả các kết nối đang cố gắng vượt qua id của cuộc trò chuyện cuối cùng (id = 1). Nếu bạn cần điều đó, hãy xem xét việc tung UPDATEvà làm điều này thay vào đó khi bạn muốn ngày mới nhất:

SELECT MAX(created_at) FROM user_chat_messages.

Id là 1 chỉ dành cho ví dụ mà tôi đã cung cấp ở đây, trong mã thực có thể là các id khác nhau tùy thuộc vào cuộc trò chuyện. Sự bế tắc đó xảy ra khi người dùng gửi nhiều tin nhắn đến cùng một cuộc trò chuyện (trò chuyện cũ với id 1). Tất nhiên tôi có thể chặn nút frontend js cho đến khi cuộc gọi kết thúc, v.v. nhưng vì mục đích học tập để xây dựng một thứ gì đó mạnh mẽ, tôi muốn có thể tránh bế tắc ngay cả khi nhiều yêu cầu được thực hiện để chèn tin nhắn vào cùng một cuộc trò chuyện (id trò chuyện cụ thể) trong cùng một lúc Tôi đã giải quyết vấn đề bằng cách chọn đầu tiên để cập nhật user_chat là điều đầu tiên xảy ra trong giao dịch
Kristi Jorgji
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.