Giao dịch đồng thời dẫn đến tình trạng cuộc đua với ràng buộc duy nhất về chèn


7

Tôi có một dịch vụ web (http api) cho phép người dùng tạo tài nguyên một cách cẩn thận. Sau khi xác thực và xác thực, tôi chuyển dữ liệu sang hàm Postgres và cho phép nó kiểm tra ủy quyền và tạo các bản ghi trong cơ sở dữ liệu.

Tôi đã tìm thấy một lỗi ngày hôm nay khi hai yêu cầu http đã được thực hiện trong cùng một giây khiến cho chức năng này được gọi với dữ liệu giống hệt nhau hai lần. Có một mệnh đề bên trong hàm tạo một lựa chọn trên bảng để xem giá trị có tồn tại không, nếu nó tồn tại thì tôi lấy ID và sử dụng nó cho thao tác tiếp theo của mình, nếu không thì tôi chèn dữ liệu, lấy trở lại ID và sau đó sử dụng nó trong các hoạt động tiếp theo. Dưới đây là một ví dụ đơn giản.

select id into articleId from articles where title = 'my new blog';
if articleId is null then
    insert into articles (title, content) values (_title, _content)
    returning id into articleId;
end if;
-- Continue, using articleId to represent the article for next operations...

Như bạn có thể đoán, tôi đã đọc được một dữ liệu ảo trên dữ liệu nơi cả hai giao dịch được nhập vào if articleId is null thenkhối và cố gắng chèn vào bảng. Một người đã thành công và người kia đã nổ tung vì một ràng buộc duy nhất trên một lĩnh vực.

Tôi đã xem xét cách bảo vệ chống lại điều này và tìm thấy một vài lựa chọn khác nhau nhưng dường như không có cách nào phù hợp với nhu cầu của chúng tôi vì một vài lý do và tôi đang vật lộn để tìm bất kỳ giải pháp thay thế nào.

  1. insert ... on conflict do nothing/update...Trước tiên tôi đã xem xét on conflicttùy chọn có vẻ tốt tuy nhiên tùy chọn duy nhất là do nothingsau đó không trả lại ID của bản ghi đã gây ra xung đột và do updatesẽ không hoạt động vì nó sẽ khiến các trình kích hoạt bị loại bỏ khi trong thực tế dữ liệu không thay đổi. Trong một số trường hợp, đây không phải là vấn đề nhưng trong nhiều trường hợp, điều này có thể làm mất hiệu lực các phiên người dùng không phải là điều chúng tôi có thể làm.
  2. set transaction isolation level serializable;đây có vẻ là câu trả lời hấp dẫn nhất, tuy nhiên ngay cả bộ thử nghiệm của chúng tôi cũng có thể gây ra sự phụ thuộc đọc / ghi, như ở trên, chúng tôi muốn chèn nếu có thứ gì đó không tồn tại và trả lại nếu có và tiếp tục thực hiện các thao tác tiếp theo. Nếu chúng tôi có một số giao dịch đang chờ xử lý chạy mã ở trên, nó sẽ gây ra lỗi phụ thuộc đọc / ghi như được nêu trong giao dịch-iso của tài liệu Postgres .

Làm thế nào để loại giao dịch đọc / ghi đồng thời này được xử lý?

Cả bản thân tôi và nhóm của tôi đều tự nhận là chuyên gia cơ sở dữ liệu, nói gì đến chuyên gia của Postgres nhưng cảm thấy như đây phải là một vấn đề được giải quyết, hoặc một người đã gặp phải trong quá khứ. Chúng tôi đang mở cho bất kỳ đề xuất. Nếu thông tin được cung cấp ở trên là không đủ, vui lòng bình luận và tôi sẽ thêm thông tin nếu cần.


có lẽ chỉ cập nhật các cột không có trình kích hoạt cập nhật đang xem chúng (và không thay đổi giá trị trong các cột đó) hoặc đặt if new is not distinct from old then return new; end if;ở đầu tất cả các trình kích hoạt cập nhật của bạn.
Jasen

Câu trả lời:


5

Hãy thử insertđầu tiên, với on conflict ... do nothingreturning id. Nếu giá trị đã tồn tại, bạn sẽ không nhận được kết quả nào từ câu lệnh này, vì vậy bạn phải thực hiện a selectđể lấy ID.

Nếu hai giao dịch cố gắng thực hiện việc này cùng một lúc, một trong số chúng sẽ chặn trên insert(vì cơ sở dữ liệu chưa biết liệu giao dịch kia sẽ cam kết hay phục hồi) và chỉ tiếp tục sau khi giao dịch khác kết thúc.


Cảm ơn vì điều này, một giải pháp đơn giản nhưng chúng tôi đã bỏ qua vì nó có khả năng sẽ được thực hiện ở nhiều nơi. Điều đó nói rằng, điều đó không có nghĩa là nó xấu, chỉ có nghĩa là chúng ta có một số việc phải làm!
Elliot Blackburn

Mặc dù điều này là đơn giản và nên hoạt động trong hầu hết các trường hợp, nó vẫn không thể tìm thấy hàng nào (mặc dù hàng đó ở đó) hoặc tìm một hàng trong SELECTđó, một giao dịch đồng thời đã bị xóa (chưa được cam kết). Trong tải nặng đồng thời, bạn có thể phải làm nhiều hơn (vòng lặp), giống như các câu trả lời khác minh họa.
Erwin Brandstetter

@ErwinBrandstetter Tôi đã thử nghiệm điều này. Vui lòng cung cấp một chuỗi các lệnh cho thấy vấn đề.
CL.

Tôi đã thảo luận chi tiết các vấn đề có thể xảy ra ở đây: stackoverflow.com/a/42217872/939860
Erwin Brandstetter

@ErwinBrandstetter Trong thử nghiệm của tôi, câu "CHỌN thấy cùng một ảnh chụp nhanh từ khi bắt đầu truy vấn và cũng không thể trả về hàng vô hình." có vẻ là sai [Trong hai kết nối: C1: BEGIN; C2: BẮT ĐẦU; C1: CHỨNG MINH; C2: thử cùng INSERT, các khối; C1: CAM KẾT; C2: khóa đơn; C2: CHỌN thấy hàng mới.] Một lần nữa, vui lòng cung cấp một ví dụ chứng minh sự tồn tại của vấn đề.
CL.

4

Nguyên nhân của vấn đề là, với READ COMMITTEDmức cô lập mặc định , mỗi UPSERT đồng thời (hoặc bất kỳ truy vấn nào) chỉ có thể nhìn thấy các hàng có thể nhìn thấy khi bắt đầu truy vấn. Hướng dẫn sử dụng:

Khi một giao dịch sử dụng mức cô lập này, một SELECTtruy vấn (không có mệnh đề FOR UPDATE/ SHARE) chỉ nhìn thấy dữ liệu được cam kết trước khi truy vấn bắt đầu; nó không bao giờ thấy dữ liệu không được cam kết hoặc các thay đổi được cam kết trong quá trình thực hiện truy vấn bằng các giao dịch đồng thời.

Nhưng một UNIQUEchỉ mục là tuyệt đối và vẫn phải xem xét các hàng được nhập đồng thời - ngay cả các hàng vô hình. Vì vậy, bạn có thể có một ngoại lệ cho một vi phạm duy nhất, nhưng bạn vẫn không thể thấy hàng xung đột trong cùng một truy vấn . Hướng dẫn sử dụng:

INSERTvới một ON CONFLICT DO NOTHINGđiều khoản có thể có chèn không được tiến hành cho một hàng do kết quả của một giao dịch khác có ảnh hưởng không thể nhìn thấy trong INSERTảnh chụp nhanh. Một lần nữa, đây chỉ là trường hợp trong chế độ Đọc cam kết.

"Giải pháp" vũ phu cho vấn đề này là ghi đè lên các hàng xung đột với ON CONFLICT ... DO UPDATE. Phiên bản hàng mới sau đó được hiển thị trong cùng một truy vấn. Nhưng có một số tác dụng phụ và tôi sẽ khuyên bạn nên chống lại nó. Một trong số đó là các UPDATEyếu tố kích hoạt bị sa thải - điều bạn muốn tránh một cách rõ ràng. Câu trả lời liên quan chặt chẽ về SO:

Tùy chọn còn lại là bắt đầu một lệnh mới (trong cùng một giao dịch), sau đó có thể thấy các hàng xung đột này từ truy vấn trước đó. Cả hai câu trả lời hiện tại đề nghị càng nhiều. Hướng dẫn sử dụng một lần nữa:

Tuy nhiên, SELECTkhông thấy tác động của các cập nhật trước đó được thực hiện trong giao dịch của chính nó, mặc dù chúng chưa được cam kết. Cũng lưu ý rằng hai SELECTlệnh liên tiếp có thể thấy các dữ liệu khác nhau, mặc dù chúng nằm trong một giao dịch, nếu các giao dịch khác cam kết thay đổi sau lần đầu tiên SELECTbắt đầu và trước khi lần thứ hai SELECTbắt đầu.

Nhưng bạn muốn nhiều hơn nữa :

- Tiếp tục, sử dụng articleId để thể hiện bài viết cho các hoạt động tiếp theo ...

Nếu đồng thời hoạt động ghi có thể có thể thay đổi hoặc xóa hàng, để được hoàn toàn chắc chắn, bạn cũng phải khóa các lựa chọn liên tiếp. (Hàng được chèn vẫn bị khóa.)

Và vì bạn dường như có các giao dịch rất cạnh tranh, để đảm bảo bạn thành công, hãy lặp lại cho đến khi thành công. Được gói vào một hàm plpgsql:

CREATE OR REPLACE FUNCTION f_articleid(_title text, _content text, OUT _articleid int) AS
$func$
BEGIN
   LOOP
      SELECT articleid
      FROM   articles
      WHERE  title = _title
      FOR    UPDATE          -- or maybe a weaker lock 
      INTO   _articleid;

      EXIT WHEN FOUND;

      INSERT INTO articles AS a (title, content)
      VALUES (_title, _content)
      ON     CONFLICT (title) DO NOTHING  -- (new?) _content is discarded
      RETURNING a.articleid
      INTO   _articleid;

      EXIT WHEN FOUND;
   END LOOP;
END
$func$ LANGUAGE plpgsql;

Giải thích chi tiết:


3

Tôi nghĩ rằng giải pháp tốt nhất là chỉ cần thực hiện thao tác chèn, bắt lỗi và xử lý đúng cách. Nếu bạn chuẩn bị xử lý lỗi, mức cô lập tuần tự hóa (dường như) không cần thiết cho trường hợp của bạn. Nếu bạn chưa sẵn sàng để xử lý lỗi, mức cô lập tuần tự hóa sẽ không giúp ích - nó sẽ chỉ tạo ra nhiều lỗi hơn mà bạn không chuẩn bị để xử lý.

Một lựa chọn khác là thực hiện TRÊN CONFLICT KHÔNG NÊN và sau đó nếu không có gì xảy ra, hãy theo dõi bằng cách thực hiện truy vấn bạn đang thực hiện để có được giá trị bắt buộc phải có ngay bây giờ. Nói cách khác, chuyển select id into articleId from articles where title = 'my new blog';từ bước phủ đầu sang bước chỉ được thực hiện nếu TRÊN CONFLICT KHÔNG thực sự không làm gì cả. Nếu có thể chèn một bản ghi và sau đó xóa lại, thì bạn nên làm điều này trong một vòng lặp thử lại.


Cảm ơn vì điều này, tôi sẽ xem xét xung đột không làm gì ban đầu và đọc thêm về cách bắt lỗi postgres thích hợp và xem cái nào làm cho mã dễ đọc nhất. Tôi tưởng tượng chúng ta sẽ sử dụng một số kết hợp cả hai trong suốt mã của chúng tôi bây giờ.
Elliot Blackburn
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.