Làm thế nào để tránh Bế tắc của mysql được tìm thấy khi cố gắng khóa; thử khởi động lại giao dịch '


286

Tôi có một bảng innoDB ghi lại người dùng trực tuyến. Nó được cập nhật trên mỗi trang được làm mới bởi người dùng để theo dõi những trang họ đang truy cập và ngày truy cập cuối cùng của họ vào trang web. Sau đó tôi có một cron chạy cứ sau 15 phút để XÓA các hồ sơ cũ.

Tôi đã nhận được một 'Bế tắc được tìm thấy khi cố gắng để có được khóa; hãy thử khởi động lại giao dịch 'trong khoảng 5 phút đêm qua và dường như là khi chạy INSERT vào bảng này. Ai đó có thể đề nghị làm thế nào để tránh lỗi này?

=== CHỈNH SỬA ===

Dưới đây là các truy vấn đang chạy:

Lần đầu tiên truy cập vào trang web:

INSERT INTO onlineusers SET
ip = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3

Trên mỗi trang làm mới:

UPDATE onlineusers SET
ips = 123.456.789.123,
datetime = now(),
userid = 321,
page = '/thispage',
area = 'thisarea',
type = 3
WHERE id = 888

Cron cứ sau 15 phút:

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Sau đó, một số tính toán để ghi lại một số thống kê (ví dụ: thành viên trực tuyến, khách truy cập trực tuyến).


Bạn có thể cung cấp thêm một số chi tiết về cấu trúc bảng? Có bất kỳ chỉ mục cụm hoặc không bao gồm?
Anders Abel

13
dev.mysql.com/doc/refman/5.1/en/innodb-deadlocks.html - Chạy "hiển thị trạng thái innodb của công cụ" sẽ cung cấp chẩn đoán hữu ích.
Martin

Nó không phải là một thực hành tốt để viết một cơ sở dữ liệu đồng bộ khi người dùng bước trên một trang. Cách đúng đắn để làm điều đó là giữ nó trong bộ nhớ như memcache hoặc một số hàng đợi nhanh và ghi vào db bằng cron.

Câu trả lời:


292

Một mẹo đơn giản có thể giúp với hầu hết các bế tắc là sắp xếp các hoạt động theo một thứ tự cụ thể.

Bạn gặp bế tắc khi hai giao dịch đang cố khóa hai khóa ở các lệnh ngược nhau, nghĩa là:

  • kết nối 1: khóa khóa (1), khóa khóa (2);
  • kết nối 2: khóa khóa (2), khóa khóa (1);

Nếu cả hai chạy cùng lúc, kết nối 1 sẽ khóa phím (1), kết nối 2 sẽ khóa phím (2) và mỗi kết nối sẽ chờ người kia giải phóng khóa -> bế tắc.

Bây giờ, nếu bạn thay đổi truy vấn của mình sao cho các kết nối sẽ khóa các khóa theo cùng một thứ tự, nghĩa là:

  • kết nối 1: khóa khóa (1), khóa khóa (2);
  • kết nối 2: khóa khóa ( 1 ), khóa khóa ( 2 );

nó sẽ không thể có được một bế tắc.

Vì vậy, đây là những gì tôi đề nghị:

  1. Đảm bảo rằng bạn không có truy vấn nào khác khóa truy cập nhiều hơn một khóa cùng một lúc trừ câu lệnh xóa. nếu bạn làm (và tôi nghi ngờ bạn làm), hãy đặt WHERE của họ theo (k1, k2, .. kn) theo thứ tự tăng dần.

  2. Sửa câu lệnh xóa của bạn để hoạt động theo thứ tự tăng dần:

Thay đổi

DELETE FROM onlineusers WHERE datetime <= now() - INTERVAL 900 SECOND

Đến

DELETE FROM onlineusers WHERE id IN (SELECT id FROM onlineusers
    WHERE datetime <= now() - INTERVAL 900 SECOND order by id) u;

Một lưu ý khác là tài liệu mysql đề xuất rằng trong trường hợp bế tắc, khách hàng nên thử lại tự động. bạn có thể thêm logic này vào mã máy khách của bạn. (Nói, 3 lần thử lại về lỗi cụ thể này trước khi bỏ cuộc).


2
Nếu tôi có Giao dịch (autocommit = false), ngoại lệ bị khóa. Có đủ chỉ để thử lại cùng một statement.executeUpdate () hoặc toàn bộ giao dịch hiện đã được đưa vào và nên được khôi phục + chạy lại mọi thứ đang chạy trong đó?
Ai

5
nếu bạn đã kích hoạt giao dịch, đó là tất cả hoặc không có gì. nếu bạn có ngoại lệ dưới bất kỳ hình thức nào, điều đó đảm bảo rằng toàn bộ giao dịch không có hiệu lực. trong trường hợp đó bạn sẽ muốn khởi động lại toàn bộ.
Omry Yadan

4
Việc xóa dựa trên lựa chọn trên một bảng lớn rất chậm so với xóa đơn giản
Thermech

3
Cảm ơn bạn rất nhiều, anh bạn. Mẹo 'sắp xếp các câu lệnh' đã khắc phục các sự cố khóa chết của tôi.
Miere

4
@OmryYadan Từ những gì tôi biết, trong MySQL, bạn không thể chọn truy vấn con từ cùng một bảng trong đó bạn đang thực hiện CẬP NHẬT. dev.mysql.com/doc/refman/5.7/en/update.html
artaxerxe

72

Bế tắc xảy ra khi hai giao dịch chờ nhau để có được một khóa. Thí dụ:

  • Tx 1: khóa A, rồi B
  • Tx 2: khóa B, sau đó A

Có rất nhiều câu hỏi và câu trả lời về những bế tắc. Mỗi lần bạn chèn / cập nhật / hoặc xóa một hàng, sẽ có được một khóa. Để tránh bế tắc, sau đó bạn phải đảm bảo rằng các giao dịch đồng thời không cập nhật hàng theo thứ tự có thể dẫn đến bế tắc. Nói chung, hãy cố gắng có được khóa luôn theo cùng một thứ tự ngay cả trong các giao dịch khác nhau (ví dụ: luôn luôn là bảng A trước, sau đó là bảng B).

Một lý do khác cho sự bế tắc trong cơ sở dữ liệu có thể thiếu các chỉ mục . Khi một hàng được chèn / cập nhật / xóa, cơ sở dữ liệu cần kiểm tra các ràng buộc quan hệ, nghĩa là đảm bảo các mối quan hệ nhất quán. Để làm như vậy, cơ sở dữ liệu cần kiểm tra các khóa ngoại trong các bảng liên quan. Nó có thể dẫn đến khóa khác được mua hơn hàng được sửa đổi. Hãy chắc chắn sau đó luôn có chỉ mục trên các khóa ngoại (và tất nhiên là các khóa chính), nếu không nó có thể dẫn đến khóa bảng thay vì khóa hàng . Nếu khóa bảng xảy ra, sự tranh chấp khóa cao hơn và khả năng bế tắc tăng lên.


3
Vì vậy, có lẽ vấn đề của tôi là Người dùng đã làm mới trang và do đó kích hoạt CẬP NHẬT bản ghi cùng lúc với cron đang cố chạy XÓA trên bản ghi. Tuy nhiên, tôi đang gặp lỗi trên INSERTS, do đó, cron sẽ không xóa các bản ghi vừa được tạo. Vậy làm thế nào một bế tắc có thể xảy ra trên một bản ghi chưa được chèn?
David

Bạn có thể cung cấp thêm một chút thông tin về (các) bảng và các giao dịch chính xác làm gì không?
ewernli

Tôi không thấy bế tắc có thể xảy ra như thế nào nếu chỉ có một tuyên bố cho mỗi giao dịch. Không có hoạt động khác trên các bảng khác? Không có khóa ngoại đặc biệt hoặc ràng buộc duy nhất? Không có ràng buộc xóa tầng?
ewernli

Không, không có gì đặc biệt ... Tôi cho rằng nó thuộc về bản chất của việc sử dụng bảng. một hàng đang được chèn / cập nhật mỗi lần làm mới trang từ khách truy cập. Khoảng hơn 1000 khách truy cập vào bất cứ lúc nào.
David

12

Có khả năng câu lệnh xóa sẽ ảnh hưởng đến một phần lớn của tổng số hàng trong bảng. Cuối cùng, điều này có thể dẫn đến khóa bảng được lấy khi xóa. Giữ một khóa (trong trường hợp này là khóa hàng hoặc khóa trang) và có được nhiều khóa hơn luôn là một rủi ro bế tắc. Tuy nhiên tôi không thể giải thích lý do tại sao câu lệnh chèn dẫn đến sự leo thang khóa - nó có thể phải làm với việc tách / thêm trang, nhưng ai đó biết MySQL tốt hơn sẽ phải điền vào đó.

Để bắt đầu, có thể đáng để cố gắng có được một bảng rõ ràng ngay lập tức cho câu lệnh xóa. Xem LOCK BẢNGcác vấn đề khóa bảng .


6

Bạn có thể thử để deletecông việc đó hoạt động bằng cách trước tiên chèn khóa của mỗi hàng sẽ bị xóa vào bảng tạm thời như mã giả này

create temporary table deletetemp (userid int);

insert into deletetemp (userid)
  select userid from onlineusers where datetime <= now - interval 900 second;

delete from onlineusers where userid in (select userid from deletetemp);

Phá vỡ nó như thế này là ít hiệu quả hơn nhưng nó tránh sự cần thiết phải giữ một khóa phạm vi khóa trong suốt delete.

Ngoài ra, sửa đổi các selecttruy vấn của bạn để thêm một wheremệnh đề không bao gồm các hàng cũ hơn 900 giây. Điều này tránh sự phụ thuộc vào công việc định kỳ và cho phép bạn lên lịch lại để chạy ít thường xuyên hơn.

Lý thuyết về các bế tắc: Tôi không có nhiều nền tảng về MySQL nhưng ở đây sẽ ... Điều deletenày sẽ giữ một khóa phạm vi khóa cho datetime, để ngăn các hàng khớp với wheremệnh đề của nó được thêm vào giữa giao dịch và khi tìm thấy các hàng để xóa, nó sẽ cố lấy khóa trên mỗi trang mà nó đang sửa đổi. Bạn insertsẽ có được một khóa trên trang mà nó đang chèn vào, và sau đó cố gắng để có được khóa. Thông thường, họ insertsẽ kiên nhẫn chờ khóa đó mở ra nhưng điều này sẽ bế tắc nếu deletecố gắng khóa cùng một trang insertđang sử dụng vì deletenhu cầu khóa trang đó và insertnhu cầu khóa phím đó. Điều này dường như không phù hợp với chèn, deleteinsert đang sử dụng phạm vi thời gian không trùng lặp để có thể có điều gì khác đang diễn ra.

http://dev.mysql.com/doc/refman/5.1/en/innodb-next-key-locking.html


4

Trong trường hợp ai đó vẫn đang vật lộn với vấn đề này:

Tôi đã đối mặt với vấn đề tương tự trong đó có 2 yêu cầu tấn công máy chủ cùng một lúc. Không có tình huống nào như dưới đây:

T1:
    BEGIN TRANSACTION
    INSERT TABLE A
    INSERT TABLE B
    END TRANSACTION

T2:
    BEGIN TRANSACTION
    INSERT TABLE B
    INSERT TABLE A
    END TRANSACTION

Vì vậy, tôi đã bối rối tại sao bế tắc đang xảy ra.

Sau đó, tôi thấy rằng có con tàu quan hệ cha mẹ giữa 2 bảng vì khóa ngoại. Khi tôi đang chèn một bản ghi trong bảng con, giao dịch đã thu được một khóa trên hàng của bảng cha. Ngay lập tức sau đó tôi đã cố gắng cập nhật hàng cha mẹ đang kích hoạt độ cao của khóa thành ĐỘC QUYỀN. Vì giao dịch đồng thời thứ 2 đã giữ khóa CHIA SẺ, nó đã gây ra bế tắc.

Tham khảo: https://blog.tekenlight.com/2019/02/21/database-deadlock-mysql.html


Trong trường hợp của tôi cũng vậy, có vẻ như vấn đề là một mối quan hệ khóa ngoại. Thanks1
Chris Prince

3

Đối với các lập trình viên Java sử dụng Spring, tôi đã tránh được vấn đề này bằng cách sử dụng khía cạnh AOP tự động thử lại các giao dịch chạy vào các bế tắc nhất thời.

Xem @R temTransaction Javadoc để biết thêm thông tin.


0

Tôi có một phương thức, các phần bên trong được gói trong MySqlTransaction.

Vấn đề bế tắc xuất hiện với tôi khi tôi chạy cùng một phương thức song song với chính nó.

Không có vấn đề gì khi chạy một phiên bản duy nhất của phương thức.

Khi tôi loại bỏ MySqlTransaction, tôi có thể chạy phương thức song song với chính nó mà không gặp vấn đề gì.

Chỉ chia sẻ kinh nghiệm của tôi, tôi không ủng hộ bất cứ điều gì.


0

cronlà nguy hiểm. Nếu một trường hợp của cron không hoàn thành trước khi đến hạn tiếp theo, họ có khả năng chiến đấu với nhau.

Sẽ tốt hơn nếu có một công việc đang chạy liên tục sẽ xóa một số hàng, ngủ một số, sau đó lặp lại.

Ngoài ra, INDEX(datetime)rất quan trọng để tránh bế tắc.

Nhưng, nếu kiểm tra datetime bao gồm nhiều hơn, giả sử, 20% của bảng, DELETEsẽ thực hiện quét bảng. Các phần nhỏ hơn bị xóa thường xuyên hơn là một cách giải quyết.

Một lý do khác để đi với khối nhỏ hơn là để khóa ít hàng hơn.

Dòng dưới cùng:

  • INDEX(datetime)
  • Nhiệm vụ liên tục chạy - xóa, ngủ một phút, lặp lại.
  • Để đảm bảo rằng nhiệm vụ trên chưa chết, hãy có một công việc định kỳ với mục đích duy nhất là khởi động lại khi thất bại.

Các kỹ thuật xóa khác: http://mysql.rjweb.org/doc.php/deletebig

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.