KEY NGOẠI TỆ user_chat_messages_user_chat_id_foreign
là 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_foreign
khóa ngoại vì nó tham chiếu users
bả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_foreign
khó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ừ INSERT
tuyê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 UPDATE
lệ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_at
cột trong user_chats
bảng cả. Thay vào đó, hãy thêm một chỉ mục tổng hợp trên user_chat_messages
cho 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_at
thời gian mới nhất trong user_chats
bả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 UPDATE
và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_foreign
khó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.
chat_user_messages.Id
? Bạn có thể đăng DDL cho các bảng không?