Sự cố khóa với XÓA / XÓA đồng thời trong PostgreSQL


35

Điều này khá đơn giản, nhưng tôi gặp khó khăn với những gì PG làm (v9.0). Chúng tôi bắt đầu với một bảng đơn giản:

CREATE TABLE test (id INT PRIMARY KEY);

và một vài hàng:

INSERT INTO TEST VALUES (1);
INSERT INTO TEST VALUES (2);

Sử dụng công cụ truy vấn JDBC yêu thích của tôi (ExecuteQuery), tôi kết nối hai cửa sổ phiên với db nơi bảng này tồn tại. Cả hai đều là giao dịch (nghĩa là tự động cam kết = sai). Hãy gọi chúng là S1 và S2.

Cùng một mã cho mỗi:

1:DELETE FROM test WHERE id=1;
2:INSERT INTO test VALUES (1);
3:COMMIT;

Bây giờ, chạy cái này trong chuyển động chậm, thực hiện từng cái một trong các cửa sổ.

S1-1 runs (1 row deleted)
S2-1 runs (but is blocked since S1 has a write lock)
S1-2 runs (1 row inserted)
S1-3 runs, releasing the write lock
S2-1 runs, now that it can get the lock. But reports 0 rows deleted. HUH???
S2-2 runs, reports a unique key constraint violation

Bây giờ, điều này hoạt động tốt trong SQLServer. Khi S2 thực hiện xóa, nó báo cáo xóa 1 hàng. Và sau đó chèn của S2 hoạt động tốt.

Tôi nghi ngờ rằng PostgreSQL đang khóa chỉ mục trong bảng nơi hàng đó tồn tại, trong khi SQLServer khóa giá trị khóa thực tế.

Tôi có đúng không Điều này có thể được thực hiện để làm việc?

Câu trả lời:


39

Cả Mat và Erwin đều đúng, và tôi chỉ thêm một câu trả lời khác để mở rộng hơn nữa về những gì họ nói theo cách không phù hợp trong một bình luận. Vì câu trả lời của họ dường như không làm hài lòng tất cả mọi người, và có một gợi ý rằng các nhà phát triển PostgreQuery nên được tư vấn, và tôi là một, tôi sẽ giải thích.

Điểm quan trọng ở đây là theo tiêu chuẩn SQL, trong một giao dịch chạy ở READ COMMITTEDmức cô lập giao dịch, hạn chế là không thể nhìn thấy công việc của các giao dịch không được cam kết. Khi công việc của các giao dịch cam kết trở nên hữu hình là phụ thuộc vào việc thực hiện. Những gì bạn đang chỉ ra là một sự khác biệt trong cách hai sản phẩm đã chọn để thực hiện điều đó. Không thực hiện là vi phạm các yêu cầu của tiêu chuẩn.

Dưới đây là những gì xảy ra trong PostgreSQL, chi tiết:

S1-1 chạy (đã xóa 1 hàng)

Hàng cũ được đặt đúng vị trí, vì S1 vẫn có thể quay lại, nhưng S1 hiện giữ khóa trên hàng để bất kỳ phiên nào khác cố gắng sửa đổi hàng sẽ chờ xem liệu S1 có cam kết hay quay lại không. Bất kỳ lần đọc nào của bảng vẫn có thể thấy hàng cũ, trừ khi họ cố gắng khóa nó bằng SELECT FOR UPDATEhoặc SELECT FOR SHARE.

S2-1 chạy (nhưng bị chặn do S1 có khóa ghi)

Bây giờ S2 phải chờ xem kết quả của S1. Nếu S1 quay lại thay vì cam kết, S2 sẽ xóa hàng. Lưu ý rằng nếu S1 chèn một phiên bản mới trước khi quay trở lại, phiên bản mới sẽ không bao giờ tồn tại ở góc độ của bất kỳ giao dịch nào khác, phiên bản cũ cũng sẽ không bị xóa khỏi quan điểm của bất kỳ giao dịch nào khác.

S1-2 chạy (chèn 1 hàng)

Hàng này độc lập với hàng cũ. Nếu đã có bản cập nhật của hàng với id = 1, phiên bản cũ và mới sẽ có liên quan và S2 có thể xóa phiên bản cập nhật của hàng khi nó bị bỏ chặn. Rằng một hàng mới xảy ra có cùng giá trị với một số hàng tồn tại trong quá khứ không làm cho nó giống như một phiên bản cập nhật của hàng đó.

S1-3 chạy, giải phóng khóa ghi

Vì vậy, những thay đổi của S1 vẫn tồn tại. Một hàng đã biến mất. Một hàng đã được thêm vào.

S2-1 chạy, bây giờ nó có thể có được khóa. Nhưng báo cáo 0 hàng đã bị xóa. HUH???

Điều xảy ra trong nội bộ, là có một con trỏ từ một phiên bản của một hàng sang phiên bản tiếp theo của cùng một hàng nếu nó được cập nhật. Nếu hàng bị xóa, không có phiên bản tiếp theo. Khi một READ COMMITTEDgiao dịch thức tỉnh từ một khối trên một xung đột ghi, nó sẽ theo chuỗi cập nhật đó đến hết; nếu hàng chưa bị xóa và nếu nó vẫn đáp ứng các tiêu chí lựa chọn của truy vấn thì nó sẽ được xử lý. Hàng này đã bị xóa, vì vậy truy vấn của S2 tiếp tục.

S2 có thể hoặc không thể đến hàng mới trong quá trình quét bảng. Nếu đúng như vậy, nó sẽ thấy rằng hàng mới đã được tạo sau khi DELETEcâu lệnh của S2 bắt đầu và do đó không phải là một phần của tập hợp các hàng hiển thị cho nó.

Nếu PostgreSQL khởi động lại toàn bộ câu lệnh XÓA của S2 ngay từ đầu bằng một ảnh chụp nhanh mới, thì nó sẽ hoạt động giống như SQL Server. Cộng đồng PostgreSQL đã không chọn làm điều đó vì lý do hiệu suất. Trong trường hợp đơn giản này, bạn sẽ không bao giờ nhận thấy sự khác biệt về hiệu suất, nhưng nếu bạn bị mười triệu hàng vào một DELETEkhi bạn bị chặn, bạn chắc chắn sẽ làm được. Có sự đánh đổi ở đây, nơi PostgreSQL đã chọn hiệu năng, vì phiên bản nhanh hơn vẫn tuân thủ các yêu cầu của tiêu chuẩn.

S2-2 chạy, báo cáo vi phạm ràng buộc khóa duy nhất

Tất nhiên, hàng đã tồn tại. Đây là phần ít ngạc nhiên nhất của bức tranh.

Mặc dù có một số hành vi đáng ngạc nhiên ở đây, mọi thứ đều phù hợp với tiêu chuẩn SQL và trong giới hạn của "đặc thù triển khai" theo tiêu chuẩn. Chắc chắn có thể đáng ngạc nhiên nếu bạn cho rằng một số hành vi của việc triển khai khác sẽ có mặt trong tất cả các lần triển khai, nhưng PostgreQuery cố gắng hết sức để tránh các lỗi tuần tự hóa ở READ COMMITTEDmức độ cô lập và cho phép một số hành vi khác với các sản phẩm khác để đạt được điều đó.

Bây giờ, cá nhân tôi không phải là một fan hâm mộ lớn của READ COMMITTEDmức độ cô lập giao dịch trong bất kỳ triển khai sản phẩm nào . Tất cả đều cho phép các điều kiện chủng tộc để tạo ra các hành vi đáng ngạc nhiên từ quan điểm giao dịch. Một khi ai đó đã quen với những hành vi kỳ lạ được cho phép bởi một sản phẩm, họ có xu hướng xem xét "bình thường" và sự đánh đổi được chọn bởi một sản phẩm kỳ quặc khác. Nhưng mỗi sản phẩm phải thực hiện một số loại đánh đổi cho bất kỳ chế độ nào không thực sự được thực hiện như SERIALIZABLE. Trường hợp các nhà phát triển PostgreQuery đã chọn cách vẽ đường READ COMMITTEDnày là để giảm thiểu việc chặn (đọc không chặn ghi và viết không chặn đọc) và để giảm thiểu khả năng thất bại nối tiếp.

Tiêu chuẩn yêu cầu SERIALIZABLEcác giao dịch là mặc định, nhưng hầu hết các sản phẩm không làm điều đó bởi vì nó gây ra hiệu suất vượt qua các mức cô lập giao dịch lỏng lẻo hơn. Một số sản phẩm thậm chí không cung cấp các giao dịch thực sự tuần tự hóa khi SERIALIZABLEđược chọn - đáng chú ý nhất là Oracle và các phiên bản của PostgreQuery trước 9.1. Nhưng sử dụng SERIALIZABLEcác giao dịch thực sự là cách duy nhất để tránh các tác động đáng ngạc nhiên từ các điều kiện cuộc đua và SERIALIZABLEcác giao dịch luôn phải chặn để tránh các điều kiện cuộc đua hoặc quay trở lại một số giao dịch để tránh tình trạng cuộc đua đang phát triển. Việc thực hiện SERIALIZABLEgiao dịch phổ biến nhất là Khóa hai pha nghiêm ngặt (S2PL) có cả lỗi chặn và tuần tự hóa (dưới dạng các khóa chết).

Tiết lộ đầy đủ: Tôi đã làm việc với Dan Cổng của MIT để thêm các giao dịch thực sự tuần tự hóa vào PostgreQuery phiên bản 9.1 bằng cách sử dụng một kỹ thuật mới có tên là Cách ly ảnh chụp nối tiếp.


Tôi tự hỏi nếu một cách thực sự rẻ (cheesy?) Để thực hiện công việc này là phát hành hai XÓA sau đó là INSERT. Trong thử nghiệm giới hạn (2 luồng) của tôi, nó hoạt động tốt, nhưng cần kiểm tra thêm để xem liệu điều đó có giữ được nhiều luồng không.
DaveyBob

Miễn là bạn đang sử dụng READ COMMITTEDcác giao dịch, bạn có một điều kiện cuộc đua: điều gì sẽ xảy ra nếu một giao dịch khác chèn một hàng mới sau lần đầu tiên DELETEbắt đầu và trước khi lần thứ hai DELETEbắt đầu? Với các giao dịch ít nghiêm ngặt hơn SERIALIZABLEhai cách chính để đóng các điều kiện cuộc đua là thông qua việc thúc đẩy một cuộc xung đột (nhưng điều đó không giúp ích gì khi hàng bị xóa) và hiện thực hóa một cuộc xung đột. Bạn có thể cụ thể hóa xung đột bằng cách có bảng "id" được cập nhật cho mỗi hàng bị xóa hoặc bằng cách khóa rõ ràng bảng. Hoặc sử dụng thử lại khi có lỗi.
kgrittn

Thử lại là được. Cảm ơn rất nhiều cho cái nhìn sâu sắc có giá trị!
DaveyBob

21

Tôi tin rằng đây là do thiết kế, theo mô tả về mức độ cô lập đã đọc cam kết cho PostgreQuery 9.2:

CẬP NHẬT, XÓA, CHỌN ĐỂ CẬP NHẬT và CHỌN CHO CHIA SẺ các lệnh hoạt động giống như CHỌN về mặt tìm kiếm các hàng đích: chúng sẽ chỉ tìm thấy các hàng đích đã được cam kết kể từ thời điểm bắt đầu lệnh 1 . Tuy nhiên, một hàng mục tiêu như vậy có thể đã được cập nhật (hoặc xóa hoặc khóa) bởi một giao dịch đồng thời khác tại thời điểm nó được tìm thấy. Trong trường hợp này, trình cập nhật sẽ chờ đợi giao dịch cập nhật đầu tiên được cam kết hoặc khôi phục (nếu nó vẫn đang được tiến hành). Nếu trình cập nhật đầu tiên quay trở lại, thì hiệu ứng của nó bị phủ định và trình cập nhật thứ hai có thể tiến hành cập nhật hàng tìm thấy ban đầu. Nếu trình cập nhật đầu tiên cam kết, trình cập nhật thứ hai sẽ bỏ qua hàng nếu trình cập nhật đầu tiên xóa nó 2, nếu không, nó sẽ cố gắng áp dụng hoạt động của nó cho phiên bản cập nhật của hàng.

Các dòng bạn chèn vào S1không tồn tại được nêu khi S2's DELETEbắt đầu. Vì vậy, nó sẽ không được nhìn thấy bằng cách xóa S2theo ( 1 ) ở trên. Một trong đó S1đã xóa được bỏ qua bởi S2's DELETEtheo ( 2 ).

Vì vậy S2, trong xóa, không có gì. Khi chèn đi cùng, người ta sẽ thấy S1chèn:

Vì chế độ Đọc được cam kết bắt đầu mỗi lệnh bằng một ảnh chụp nhanh mới bao gồm tất cả các giao dịch được cam kết ngay lập tức, các lệnh tiếp theo trong cùng một giao dịch sẽ thấy tác động của giao dịch đồng thời được cam kết trong mọi trường hợp . Vấn đề ở trên là liệu một lệnh có nhìn thấy một khung nhìn hoàn toàn nhất quán của cơ sở dữ liệu hay không.

Vì vậy, cố gắng chèn bởi S2thất bại với vi phạm ràng buộc.

Tiếp tục đọc tài liệu đó, sử dụng đọc lặp lại hoặc thậm chí tuần tự hóa sẽ không giải quyết được hoàn toàn vấn đề của bạn - phiên thứ hai sẽ thất bại với lỗi tuần tự hóa khi xóa.

Điều này sẽ cho phép bạn thử lại giao dịch mặc dù.


Cảm ơn Mat. Trong khi đó dường như là những gì đang xảy ra, dường như có một lỗ hổng trong logic đó. Dường như với tôi, ở cấp độ READ_OMMITTED, sau đó hai câu lệnh này phải thành công trong một tx: XÓA TỪ kiểm tra WHERE ID = 1 INSERT INTO test GIÁ TRỊ (1) Ý tôi là, nếu tôi xóa hàng rồi chèn hàng, sau đó chèn nên thành công. SQLServer có quyền này. Như vậy, tôi đang rất khó khăn trong việc xử lý tình huống này trong một sản phẩm phải làm việc với cả hai cơ sở dữ liệu.
DaveyBob

11

Tôi hoàn toàn đồng ý với câu trả lời tuyệt vời của @ Mat . Tôi chỉ viết một câu trả lời khác, vì nó không phù hợp với một bình luận.

Trả lời nhận xét của bạn: DELETES2 trong đã được nối trên một phiên bản hàng cụ thể. Vì điều này đã bị S1 giết chết trong thời gian đó, S2 coi đó là thành công. Mặc dù không rõ ràng từ một cái nhìn nhanh chóng, chuỗi các sự kiện hầu như là như thế này:

   S1 XÓA thành công  
XÓA S2 (thành công bằng proxy - XÓA từ S1)  
   S1 xác nhận lại giá trị đã xóa gần như trong thời gian này  
S2 INSERT không thành công với vi phạm ràng buộc khóa duy nhất

Tất cả là do thiết kế. Bạn thực sự cần phải sử dụng SERIALIZABLEcác giao dịch cho các yêu cầu của bạn và đảm bảo bạn thử lại thất bại nối tiếp.


1

Sử dụng khóa chính DEAXRABLE và thử lại.


cảm ơn vì tiền boa, nhưng việc sử dụng DEAXRABLE không tạo ra sự khác biệt nào cả. Các tài liệu đọc như nó nên có, nhưng không.
DaveyBob

-2

Chúng tôi cũng phải đối mặt với vấn đề này. Giải pháp của chúng tôi là thêm select ... for updatetrước delete from ... where. Mức cô lập phải được đọc Cam kết.

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.