2014-10-06 15 views
5

Trong khi viết một số truy vấn SQL cho PostgreSQL, tôi phát hiện một số hành vi bất thường, mà tôi thấy hơi đáng lo ngại.Kết quả truy vấn PostgreSQL sai với khóa rõ ràng và giao dịch đồng thời

Giả sử chúng ta có bảng "test" sau:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 1 | A  | 2014-01-01 00:00:00 | 
| 2 | A  | 2014-01-02 00:00:00 | 
| 3 | B  | 2014-01-03 00:00:00 | 
| 4 | B  | 2014-01-04 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
| 6 | B  | 2014-01-06 00:00:00 | 
| 7 | A  | 2014-01-07 00:00:00 | 
| 8 | B  | 2014-01-08 00:00:00 | 
+----+-------+---------------------+ 

Có hai giao dịch, A và B, chạy song song.

A: begin;   /* Begin transaction A */ 
B: begin;   /* Begin transaction B */ 
A: select * from test where id = 1 for update; /* Lock one row */ 
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */ 
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */ 
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */ 
A: commit; 

Ngay sau khi giao dịch A cam kết và giải phóng hàng với id = 1, truy vấn chặn giao dịch B trả về kết quả sau đây:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 1 | A  | 2014-01-09 00:00:00 | 
| 2 | A  | 2014-01-02 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
+----+-------+---------------------+ 

Những hàng được chắc chắn nhất không sắp xếp theo "created_at "và hàng có id = 1 không được nằm trong số các hàng được trả lại. Thực tế là các giao dịch A và B đang chạy đồng thời, đã dẫn đến kết quả sai trong giao dịch B, điều này sẽ không xảy ra nếu các giao dịch đã được thực hiện sau giao dịch kia. Điều này có vẻ giống như một sự vi phạm giao dịch cô lập.

Đây có phải là lỗi không?

Nếu đây không phải là lỗi và những kết quả này được mong đợi, điều này có nghĩa là gì về độ tin cậy của kết quả do DB trả về? Nếu tôi có một môi trường đồng thời cao và mã tiếp theo dựa vào các hàng thực sự được sắp xếp theo ngày, sẽ có lỗi.

Tuy nhiên, nếu chúng tôi chạy cùng một chuỗi các lệnh như trên, nhưng thay thế các báo cáo cập nhật như sau:

update test set value = 'B', created_at = '2014-01-09 00:00:00' where id = 1; 

... sau đó truy vấn chặn trả về kết quả chính xác:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 2 | A  | 2014-01-02 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
| 7 | A  | 2014-01-07 00:00:00 | 
+----+-------+---------------------+ 

Trong trường hợp này, truy vấn bị chặn có được thực thi hai lần vì kết quả ban đầu của nó bị vô hiệu không?

Tôi quan tâm nhất đến PostgreSQL, nhưng tôi cũng muốn biết nếu đây là trường hợp với RDBMS khác hỗ trợ khóa cấp hàng, chẳng hạn như Oracle, SQL Server và MySQL.

+0

Trong MySQL, lựa chọn đầu tiên trong Phiên b đã bị chặn và đợi cho đến phiên A cam kết hoặc cuộn lại. Trong Oracle, điều này khó tái tạo vì không có mệnh đề 'LIMIT' và cách tiếp cận thông thường sử dụng một bảng dẫn xuất với' rownum' và thứ tự bằng cách thay đổi hoàn toàn ý nghĩa của truy vấn. –

Trả lời

5

Có một vài điều đang diễn ra tại đây. Đầu tiên, đây là hành vi được ghi lại. Thứ hai, bạn không nhìn thấy toàn bộ câu chuyện, bởi vì bạn không cố gắng cập nhật bất cứ điều gì trong phiên "B".

Điều này có vẻ như vi phạm cách ly giao dịch.

Phụ thuộc vào mức độ cách ly bạn đang chạy. PostgreSQL's default transaction isolation levelREAD COMMITTED.

Đây là documented behavior trong PostgreSQL.

Có thể cho một lệnh SELECT chạy ở ĐỌC CAM KẾT giao dịch mức cô lập và sử dụng ORDER BY và một điều khoản khóa để hàng trở lại trong trật tự. Điều này là do ORDER BY được áp dụng trước tiên. Lệnh sắp xếp kết quả, nhưng sau đó có thể chặn cố gắng lấy khóa trên một hoặc nhiều hàng. Khi SELECT unblocks, một số giá trị cột đặt hàng có thể đã được sửa đổi, dẫn đến các hàng đó xuất hiện không đúng thứ tự (mặc dù chúng theo thứ tự về các giá trị cột ban đầu).

Một giải pháp thay thế (cũng được viết thành tài liệu, cùng liên kết) là di chuyển FOR UPDATE vào truy vấn con, nhưng điều này yêu cầu khóa bàn.

Để xem những gì PostgreSQL thực sự thực hiện trong trường hợp này, hãy chạy bản cập nhật trong phiên "B".

create table test (
    id integer primary key, 
    value char(1) not null, 
    created_at timestamp not null 
); 
insert into test values 
(1, 'A', '2014-01-01 00:00:00'), 
(2, 'A', '2014-01-02 00:00:00'), 
(3, 'B', '2014-01-03 00:00:00'), 
(4, 'B', '2014-01-04 00:00:00'), 
(5, 'A', '2014-01-05 00:00:00'), 
(6, 'B', '2014-01-06 00:00:00'), 
(7, 'A', '2014-01-07 00:00:00'), 
(8, 'B', '2014-01-08 00:00:00'); 
 
A: begin;   /* Begin transaction A */ 
B: begin;   /* Begin transaction B */ 
A: select * from test where id = 1 for update; /* Lock one row */ 
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */ 
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */ 
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */ 
A: commit; 
B: update test set value = 'C' where id in (select id from test where value = 'A' order by created_at limit 3); /* Updates 3 rows */ 
B: commit; 

Bây giờ, nhìn vào bàn.

 
scratch=# select * from test order by id; 
id | value |  created_at  
----+-------+--------------------- 
    1 | A  | 2014-01-09 00:00:00 
    2 | C  | 2014-01-02 00:00:00 
    3 | B  | 2014-01-03 00:00:00 
    4 | B  | 2014-01-04 00:00:00 
    5 | C  | 2014-01-05 00:00:00 
    6 | B  | 2014-01-06 00:00:00 
    7 | C  | 2014-01-07 00:00:00 
    8 | B  | 2014-01-08 00:00:00 

Phiên "A" đã thành công trong việc cập nhật hàng có id 1 đến '2014-01-09'. Phiên "B" đã thành công trong việc cập nhật ba hàng còn lại có giá trị là 'A'. Câu lệnh cập nhật nhận được các khóa trên số id 2, 5 và 7; chúng ta biết rằng bởi vì đó là những hàng thực sự được cập nhật. Các lựa chọn công bố trước đó nhốt hàng khác nhau - các hàng 1, 2, và 5.

Bạn có thể chặn cập nhật phiên B nếu bạn bắt đầu một phiên terminal thứ ba, và khóa hàng 7 để cập nhật.

+0

Cảm ơn bạn đã làm rõ, tôi không biết hành vi này được ghi lại. Tôi vẫn còn mới với PostgreSQL, nhưng không bao giờ ngừng ngạc nhiên bởi tất cả sức mạnh và khả năng mà nó cung cấp! – Jaan

Các vấn đề liên quan