Trang này được dịch bằng máy. Đọc bản gốc tiếng Anh. English

Alexey Kovyazin, 08-Tháng 5-2019, (c) IBSurgeon

Xung đột cập nhật (thường được gọi là «deadlock») xảy ra trong các ứng dụng Firebird thực hiện cập nhật dữ liệu đồng thời với cường độ cao.

Như bạn có thể thấy từ bài viết « Transactions in Firebird», có 3 loại lỗi chứa từ khóa «deadlock»:

Code
deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN

lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN

lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is NNN

Cả 3 loại xung đột cập nhật đều có điểm chung là xung đột cập nhật thường là kết quả của 2 thao tác sửa đổi cố gắng sửa đổi cùng một bản ghi.

Các tùy chọn có thể là: UPDATE, DELETE SELECT WITH LOCK, MERGE, UPDATE OR INSERT.

Việc theo dõi thao tác «nạn nhân» - thao tác nhận được thông báo lỗi là tương đối dễ dàng - chỉ cần đặt nó vào try… catch và ghi lại các tham số được sử dụng trong truy vấn gây ra lỗi.

Tuy nhiên, với cách tiếp cận này, rất khó để theo dõi «người thắng» - tức là, giao dịch đồng thời nơi cập nhật được thực hiện thành công mà không có lỗi.

Để giúp các nhà phát triển điều tra xung đột cập nhật, Firebird đưa vào thông báo lỗi tham chiếu đến giao dịch đồng thời - tức là, giao dịch nơi cập nhật đồng thời chưa được cam kết. Cùng với Trace API, điều này cho chúng ta khả năng theo dõi cả hai thao tác xung đột.

Hãy xem xét các bước thực tế để thực hiện điều này.

Đầu tiên, chúng ta cần cấu hình tracing. Để làm điều này, hãy tạo tệp văn bản (trong trường hợp của chúng ta là C:\Temp\mytrace.conf) với nội dung sau:

Code
database
{
	enabled = true
	include_filter="(UPDATE%)|(DELETE%)|(MERGE%)|(SELECT%FROM%WITH LOCK)|(UPDATE OR INSERT%)"
	log_statement_finish = true
	log_errors = true
	include_gds_codes="deadlock"
	log_initfini = false
	time_threshold = 0
	max_sql_length = 65000

}

Tệp cấu hình trên bật tracing cho tất cả các cơ sở dữ liệu, vì vậy nếu bạn có nhiều hơn 1 cơ sở dữ liệu đang hoạt động, tốt hơn nên chỉ định tên cơ sở dữ liệu trong tệp cấu hình, tức là:

Code
Database=mydatabase.fdb
{
  enabled = true
…
}

Hãy xem xét các tham số quan trọng nhất trong tệp cấu hình:

Tham số include filter

Code
include_filter="(UPDATE%)|(DELETE%)|(MERGE%)|(SELECT%FROM%WITH LOCK)|(UPDATE OR INSERT%)"

Điều này có nghĩa là chúng ta chỉ muốn theo dõi các câu lệnh UPDATE và DELETE và bỏ qua các câu lệnh select. Cần phải lọc ra càng nhiều câu lệnh càng tốt, vì trong hệ thống sản xuất chịu tải cao, số lượng bản ghi trong tệp nhật ký văn bản có thể quá lớn để phân tích.

Thông thường, chúng ta biết bảng nào tham gia vào xung đột cập nhật, ý tưởng tốt là đưa nó vào bộ lọc để giảm số lượng cập nhật cần phân tích:

Code
include_filter="(UPDATE T1%)|(DELETE FROM T1%)|(MERGE INTO T1%)|(SELECT% FROM T1%WITH lock%)|(UPDATE OR INSERT INTO T1%)"

Điều gì xảy ra nếu các thao tác cập nhật và xóa được thực hiện bởi các stored procedures? Điều này làm cho toàn bộ quá trình khó khăn hơn: thêm vào bộ lọc tất cả các stored procedures có thể tham gia vào các cập nhật đồng thời, và cũng bật ghi nhật ký các procedures:

Code
include_filter="(UPDATE T1%)|(DELETE FROM T1%)|(MERGE INTO T1%)|(SELECT% FROM T1%with lock%)|(UPDATE OR INSERT INTO T1%)|(%SP_UPDATE_T1%)|(%SP_DEL_T1%)"
log_procedures=true

Tham số ghi nhật ký lỗi

Sau đó, chúng ta cần chỉ định ghi nhật ký lỗi và giới hạn nó ở các deadlock.

Code
log_errors = true
include_gds_codes="deadlock"

Xin lưu ý: tham số include_gds_codes chỉ xuất hiện trong Firebird 3.0.2, vì vậy nếu bạn sử dụng 2.5 hoặc 3.0.0 hoặc 3.0.1, không thể chỉ lọc các lỗi deadlock, do đó tất cả các lỗi sẽ được in vào nhật ký.

Trình diễn

Để trình diễn toàn bộ quá trình, hãy chuẩn bị cơ sở dữ liệu thử nghiệm - để đơn giản, sẽ có 1 bảng với 1 cột khóa chính.

Code
C:\HQbird\Firebird30>isql
Use CONNECT or CREATE DATABASE to specify a database
SQL> create database "c:\temp\testdeadlock.fdb" user "SYSDBA" password "masterkey";
SQL> create table t1(i1 integer not null primary key);
SQL> insert into t1(i1) values(1);
SQL> commit; exit;

Sau đó, chúng ta cần bắt đầu phiên trace:

Code
fbtracemgr.exe -se localhost:service_mgr -start -conf "C:\temp\mytrace.conf" -user SYSDBA -pass masterkey

Trong trường hợp này, fbtracemgr in nhật ký ra màn hình, nhưng trong sản xuất, tất nhiên chúng ta cần chuyển hướng nó đến một tệp.

Sau khi bắt đầu thành công, fbtracemgr in ra một cái gì đó như thế này:

Code
Trace session ID 4 started

Sau đó, hãy bắt đầu 2 phiên isql và thử mô phỏng xung đột cập nhật. Trong bảng dưới đây, chúng ta có 2 cột cho 2 phiên isql. Mỗi hàng đại diện cho một thời điểm khi các lệnh được thực hiện.

Thời gian isql phiên 1 isql phiên 2
T1 <br>isql -user SYSDBA -pass masterkey localhost:c:\temp\testdeadlock.fdb<br>Database: localhost:c:\temp\testdeadlock.fdb, User: SYSDBA<br>SQL> set transaction wait;<br>Commit current transaction (y/n)?y<br>Committing.<br>SQL> select current_transaction from rdb$database;<br> CURRENT_TRANSACTION<br>=====================<br> 15<br>SQL> select * from t1;<br> I1<br>============<br> 2<br>SQL><br>
T2 <br>isql -user SYSDBA -pass masterkey localhost:c:\temp\testdeadlock.fdb<br>Database: localhost:c:\temp\testdeadlock.fdb, User: SYSDBA<br>SQL> set transaction wait;<br>Commit current transaction (y/n)?y<br>Committing.<br>SQL> select current_transaction from rdb$database;<br> CURRENT_TRANSACTION<br>=====================<br> 20<br>SQL> select * from t1;<br> I1<br>============<br> 2<br>SQL><br>
Vì vậy, tại điểm bắt đầu, chúng ta có thể thấy rằng 2 giao dịch được bắt đầu, #15 và #20. Sau đó, hãy thử thực hiện cập nhật đồng thời cùng một bản ghi:
T3 <br>SQL> UPDATE T1 SET i1=5 where i1=2;<br>
T4 <br>SQL> UPDATE T1 SET i1=100 where i1=2;<br>
T5 <br>SQL> commit;<br>SQL><br> <br>Statement failed, SQLSTATE = 40001<br>deadlock<br>-update conflicts with concurrent update<br>-concurrent transaction number is 15<br>SQL><br>

Chúng ta đã bắt đầu UPDATE trong phiên 1 nhưng không cam kết nó.

Trong phiên 2, UPDATE «bị treo» - nó chờ đợi sự kết thúc của giao dịch đồng thời, và khi chúng ta đã cam kết UPDATE trong phiên 1, phiên 2 nhận được lỗi:

Code
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 15
SQL>

Vì vậy, chúng ta có một tình huống điển hình trong trường hợp xung đột cập nhật với giao dịch chờ.

Hãy xem xét nhật ký trace để tìm bằng chứng về xung đột cập nhật.

Trong trường hợp này, nhật ký trace rất ngắn, nhưng trong trường hợp hệ thống sản xuất, nó có thể dài hơn nhiều, nhưng cách tiếp cận để tìm deadlock là giống nhau.

Code
fbtracemgr.exe -se localhost:service_mgr -start -conf "C:\temp\fbtrace.conf" -user SYSDBA -pass masterkey
Trace session ID 4 started

2019-05-07T11:28:43.9850 (2692:00000000018C1940) EXECUTE_STATEMENT_FINISH
        C:\TEMP\TESTDEADLOCK.FDB (ATT_12, SYSDBA:NONE, NONE, TCPv6:::1/52938)
        C:\HQbird\Firebird30\isql.exe:8828
                (TRA_15, CONCURRENCY | WAIT | READ_WRITE)

Statement 52:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=5 where i1=2
0 records fetched
      0 ms, 2 read(s), 21 fetch(es), 3 mark(s)

2019-05-07T11:29:45.0190 (2692:00000000018C0040) FAILED EXECUTE_STATEMENT_FINISH
        C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
        C:\HQbird\Firebird30\isql.exe:7032
                (TRA_20, CONCURRENCY | WAIT | READ_WRITE)

Statement 55:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=100 where i1=2
0 records fetched
  40242 ms, 19 fetch(es), 2 mark(s)

2019-05-07T11:29:45.0190 (2692:00000000018C0040) ERROR AT JStatement::execute
        C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
        C:\HQbird\Firebird30\isql.exe:7032
335544336 : deadlock
335544451 : update conflicts with concurrent update
335544878 : concurrent transaction number is 15

Như bạn có thể thấy, chúng ta có các bản ghi trong nhật ký về cả hai cập nhật - thành công và thất bại, và thông báo lỗi về deadlock.

Hãy xem xét cách phân tích nhật ký:

Trong thông báo lỗi, hãy xác định định danh của giao dịch:

Code
 2019-05-07T11:29:45.0190 (2692:00000000018C0040) ERROR AT JStatement::execute
        C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
        C:\HQbird\Firebird30\isql.exe:7032
335544336 : deadlock
335544451 : update conflicts with concurrent update
335544878 : concurrent transaction number is 15

Gần bản ghi về lỗi (thường ngay phía trên nó), bạn sẽ thấy câu lệnh thất bại:

Code
2019-05-07T11:29:45.0190 (2692:00000000018C0040) FAILED EXECUTE_STATEMENT_FINISH
        C:\TEMP\TESTDEADLOCK.FDB (ATT_13, SYSDBA:NONE, NONE, TCPv6:::1/52968)
        C:\HQbird\Firebird30\isql.exe:7032
                (TRA_20, CONCURRENCY | WAIT | READ_WRITE)

Statement 55:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=100 where i1=2
0 records fetched
  40242 ms, 19 fetch(es), 2 mark(s)

Lưu ý rằng thời gian thực thi rất lớn (~40 giây) - nó chỉ ra thời gian thao tác chờ đợi kết quả của giao dịch đồng thời.

Và cuối cùng, tìm kiếm số giao dịch với thao tác UPDATE thành công: để làm điều này, hãy tìm TRA_NNN, trong đó NNN là số giao dịch. Trong trường hợp của chúng ta, nó sẽ là TRA_15:

Code
2019-05-07T11:28:43.9850 (2692:00000000018C1940) EXECUTE_STATEMENT_FINISH
        C:\TEMP\TESTDEADLOCK.FDB (ATT_12, SYSDBA:NONE, NONE, TCPv6:::1/52938)
        C:\HQbird\Firebird30\isql.exe:8828
                (TRA_15, CONCURRENCY | WAIT | READ_WRITE)

Statement 52:
-------------------------------------------------------------------------------
UPDATE T1 SET i1=5 where i1=2
0 records fetched
      0 ms, 2 read(s), 21 fetch(es), 3 mark(s)

Vì vậy, đây chính là nó - chúng ta đã tìm thấy cả hai bên của xung đột cập nhật - bên thành công và bên thất bại.

Các công cụ để xác định deadlock

Như bạn có thể thấy, việc xác định nguồn gốc của xung đột khóa/deadlock có thể khá khó khăn: vì xung đột luôn có 2 phía, và một phía “thắng” (hoàn thành mà không có thông báo lỗi), có thể khó xác định người thắng. Ví dụ, xung đột có thể xảy ra bên trong EXECUTE STATEMENT, hoặc trong stored procedure lồng nhau, hoặc bên trong trigger, hoặc có thể là sự kết hợp của tất cả những điều này.

Để xác định deadlock, IBSurgeon đã phát triển công cụ “Deadlock Analyzer”, công cụ này phân tích nhật ký trace và tạo ra các sơ đồ dễ hiểu với chi tiết về các lệnh gọi dẫn đến deadlock. Công cụ này có sẵn như một phần của Enterprise Subscription (có bản dùng thử) - đăng ký tại cc.ib-aid.com.

Ngoài ra, HQbird giới thiệu phiên bản mở rộng của plugin fbtrace, có thể được cấu hình để chỉ lưu các thay đổi và các nỗ lực thay đổi (tức là, các thao tác thất bại và xung đột khóa và deadlock).

Liên hệ với bộ phận hỗ trợ IBSurgeon để biết thêm thông tin về điều này: [email protected].

Làm thế nào để tránh deadlock?

Bây giờ bạn cần xác định giải pháp tốt nhất cho ứng dụng của mình để tránh deadlock - có thể là thay đổi các tham số giao dịch, hoặc có thể thêm xử lý lỗi bổ sung và lặp lại thao tác hoặc thực hiện các kiểm tra bổ sung ở cấp độ nghiệp vụ của ứng dụng.