12

Tôi đang triển khai một trình đọc RSS dựa trên web đơn giản sử dụng python (không thực sự có liên quan) và Postgresql (9.2 nếu có liên quan). Giản đồ cơ sở dữ liệu như sau (dựa trên định dạng RSS):Chèn hàng nếu không tồn tại dẫn đến điều kiện chủng tộc?

CREATE TABLE feed_channel 
(
    id SERIAL PRIMARY KEY, 
    name TEXT, 
    link TEXT NOT NULL, 
    title TEXT 
); 
CREATE TABLE feed_content 
(
    id SERIAL PRIMARY KEY, 
    channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE, 
    guid TEXT UNIQUE NOT NULL, 
    title TEXT, 
    link TEXT, 
    description TEXT, 
    pubdate TIMESTAMP 
); 

Khi tôi tạo ra một kênh mới (và cũng có thể truy vấn để biết thức ăn chăn nuôi được cập nhật) Tôi yêu cầu thức ăn chăn nuôi, chèn dữ liệu của nó vào bảng feed_channel, lựa chọn ID mới được chèn - hoặc hiện tại để tránh trùng lặp - và sau đó thêm dữ liệu nguồn cấp dữ liệu vào bảng feed_content. Một kịch bản điển hình sẽ là:

  1. Query url thức ăn chăn nuôi, thức ăn chăn nuôi tiêu đề lấy và tất cả các nội dung hiện tại
  2. Chèn tiêu đề thức ăn vào feed_channel nếu không tồn tại ... nếu đã tồn tại, lấy ID hiện
  3. Đối với mỗi mục nguồn cấp dữ liệu, hãy chèn vào bảng feed_content có tham chiếu đến ID kênh được lưu trữ

Đây là vấn đề "chèn nếu chưa tồn tại nhưng trả về ID có liên quan". Để giải quyết vấn đề này, tôi đã thực hiện quy trình được lưu sau đây:

CREATE OR REPLACE FUNCTION channel_insert(
    p_link feed_channel.link%TYPE, 
    p_title feed_channel.title%TYPE 
) RETURNS feed_channel.id%TYPE AS $$ 
    DECLARE 
    v_id feed_channel.id%TYPE; 
    BEGIN 
    SELECT id 
    INTO v_id 
    FROM feed_channel 
    WHERE link=p_link AND title=p_title 
    LIMIT 1; 

    IF v_id IS NULL THEN 
     INSERT INTO feed_channel(name,link,title) 
     VALUES (DEFAULT,p_link,p_title) 
     RETURNING id INTO v_id; 
    END IF; 

    RETURN v_id; 

    END; 
$$ LANGUAGE plpgsql; 

Điều này sau đó được gọi là "chọn channel_insert (liên kết, tiêu đề);" từ ứng dụng của tôi để chèn nếu chưa tồn tại và sau đó trả lại ID của hàng liên quan bất kể nó được chèn hay chỉ tìm thấy (bước 2 trong danh sách ở trên).

Công trình này tuyệt vời!

Tuy nhiên, gần đây tôi đã bắt đầu tự hỏi điều gì sẽ xảy ra nếu quy trình này được thực hiện hai lần cùng một lúc với cùng một đối số. Cho phép giả định như sau:

  1. User 1 nỗ lực để thêm một kênh mới và do đó thực hiện channel_insert
  2. Một vài ms sau, User 2 nỗ lực để thêm cùng một kênh và cũng thực hiện channel_insert
  3. User 1 của séc các hàng hiện có đã hoàn thành, nhưng trước khi quá trình chèn hoàn tất, kiểm tra của Người dùng 2 hoàn tất và cho biết không có hàng hiện có nào.

Đây có phải là điều kiện chạy tiềm năng trong PostgreSQL không? Cách tốt nhất để giải quyết vấn đề này là gì để tránh các tình huống như vậy? Có thể thực hiện toàn bộ quy trình được lưu trữ một cách nguyên tử hay không, tức là nó chỉ có thể được thực thi một lần cùng một lúc?

Một tùy chọn mà tôi đã cố gắng là tạo trường duy nhất và sau đó cố gắng chèn đầu tiên, và nếu ngoại lệ, hãy chọn mục hiện tại thay thế ... Điều này đã làm việc, tuy nhiên, trường SERIAL sẽ tăng cho mỗi lần thử, để lại rất nhiều khoảng trống trong dãy. Tôi không biết nếu đó sẽ là một vấn đề trong thời gian dài (có lẽ không), nhưng loại gây phiền nhiễu. Có lẽ đây là giải pháp ưa thích?

Cảm ơn mọi phản hồi. Mức độ phép thuật PostgreSQL này vượt xa tôi, vì vậy mọi phản hồi sẽ được đánh giá cao.

+1

Không có vấn đề gì bạn làm, hãy cẩn thận để bình thường hóa định dạng liên kết của bạn để bạn không có vấn đề trường hợp ('Www.Example.Com' và 'www.example .com'), thứ tự tham số issus ('? a = b & c = d' và'? c = d & a = b'), v.v. –

+0

Một vòng lặp chức năng plpgsql trong trường hợp vi phạm khóa trùng lặp có thể đối phó với điều kiện chủng tộc trên phía máy chủ và ở mức cô lập mặc định, là * an toàn * thông thường * rẻ nhất *: http://stackoverflow.com/questions/15939902/is-select-or-insert-in-a-function-prone-to- race-conditions/15950324 # 15950324 –

Trả lời

4

Có một "cuộc đua" không thể tránh khỏi ở đây, vì hai phiên không thể "nhìn thấy" từng hàng không quen thuộc. Trên một cuộc xung đột, một phiên có thể chỉ rollback (có thể đến một savepoint) và thử lại. Điều đó thường có nghĩa là: đề cập đến hàng mới được chèn của người khác, thay vì tạo một bản sao riêng tư.

Có vấn đề về mô hình hóa dữ liệu ở đây: feed_channel dường như có nhiều khóa ứng viên và quy tắc xếp tầng từ feed_content có thể mồ côi nhiều hàng của feed_content (tôi cho rằng content-> channel là 1 :: M relation; nhiều nội dung có thể tham chiếu đến cùng một kênh)

Cuối cùng, bảng feed_channel ít nhất cần khóa tự nhiên {link, title}. Đó là nơi chèn/không tồn tại là tất cả về. (và toàn bộ mục đích của chức năng này)

Tôi đã làm sạch chức năng một chút. Cấu trúc IF là không cần thiết, thực hiện một CH INSN CH WHN NÀO KHÔNG XÁC NHẬNđầu tiên cũng hoạt động tốt và thậm chí có thể tốt hơn.

DROP SCHEMA tmp CASCADE; 
CREATE SCHEMA tmp ; 
SET search_path=tmp; 

CREATE TABLE feed_channel 
    (id SERIAL PRIMARY KEY 
    , name TEXT 
    , link TEXT NOT NULL 
    , title TEXT NOT NULL -- part of PK :: must be not nullable 
    , CONSTRAINT feed_channel_nat UNIQUE (link,title) -- the natural key 
); 

CREATE TABLE feed_content 
    (id SERIAL PRIMARY KEY 
    , channel INTEGER REFERENCES feed_channel(id) ON DELETE CASCADE ON UPDATE CASCADE 
    , guid TEXT UNIQUE NOT NULL -- yet another primary key 
    , title TEXT -- 
    , link TEXT -- title && link appear to be yet another candidate key 
    , description TEXT 
    , pubdate TIMESTAMP 
    ); 

-- NOTE: omitted original function channel_insert() for brevity 
CREATE OR REPLACE FUNCTION channel_insert_wp(
    p_link feed_channel.link%TYPE, 
    p_title feed_channel.title%TYPE 
) RETURNS feed_channel.id%TYPE AS $body$ 
    DECLARE 
    v_id feed_channel.id%TYPE; 
    BEGIN 
     INSERT INTO feed_channel(link,title) 
     SELECT p_link,p_title 
     WHERE NOT EXISTS (SELECT * 
     FROM feed_channel nx 
     WHERE nx.link= p_link 
     AND nx.title= p_title 
     ) 
     ; 
    SELECT id INTO v_id 
    FROM feed_channel ex 
    WHERE ex.link= p_link 
    AND ex.title= p_title 
     ; 

    RETURN v_id; 

    END; 
$body$ LANGUAGE plpgsql; 

SELECT channel_insert('Bogus_link', 'Bogus_title'); 
SELECT channel_insert_wp('Bogus_link2', 'Bogus_title2'); 

SELECT * FROM feed_channel; 

Kết quả:

DROP SCHEMA 
CREATE SCHEMA 
SET 
NOTICE: CREATE TABLE will create implicit sequence "feed_channel_id_seq" for serial column "feed_channel.id" 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "feed_channel_pkey" for table "feed_channel" 
NOTICE: CREATE TABLE/UNIQUE will create implicit index "feed_channel_nat" for table "feed_channel" 
CREATE TABLE 
NOTICE: CREATE TABLE will create implicit sequence "feed_content_id_seq" for serial column "feed_content.id" 
NOTICE: CREATE TABLE/PRIMARY KEY will create implicit index "feed_content_pkey" for table "feed_content" 
NOTICE: CREATE TABLE/UNIQUE will create implicit index "feed_content_guid_key" for table "feed_content" 
CREATE TABLE 
NOTICE: type reference feed_channel.link%TYPE converted to text 
NOTICE: type reference feed_channel.title%TYPE converted to text 
NOTICE: type reference feed_channel.id%TYPE converted to integer 
CREATE FUNCTION 
NOTICE: type reference feed_channel.link%TYPE converted to text 
NOTICE: type reference feed_channel.title%TYPE converted to text 
NOTICE: type reference feed_channel.id%TYPE converted to integer 
CREATE FUNCTION 
channel_insert 
---------------- 
       1 
(1 row) 

channel_insert_wp 
------------------- 
       2 
(1 row) 

id | name | link  | title  
----+------+-------------+-------------- 
    1 |  | Bogus_link | Bogus_title 
    2 |  | Bogus_link2 | Bogus_title2 
(2 rows) 
+1

Nếu tôi thả các thủ tục lưu trữ hoàn toàn và thay vào đó chỉ cần sử dụng: 'INSERT INTO feed_channel (nguồn) CHỌN% (nguồn) s nơi không tồn tại ( SELECT 1 TỪ feed_channel ĐÂU source =% (nguồn) s );' liệu tôi có bao giờ có khả năng kích hoạt một vi phạm duy nhất giả định rằng "nguồn" có ràng buộc duy nhất không? – agnsaft

3

Vấn đề quan trọng nhất của bạn là serial không tạo khóa chính tốt cho bảng feed_channel. Khóa chính phải là (link, title) hoặc chỉ (link) nếu title có thể là null. Sau đó, bất kỳ cố gắng để chèn một nguồn cấp dữ liệu tồn tại sẽ làm tăng một lỗi khóa chính.

BTW v_id sẽ null bất cứ khi nào titlenull:

WHERE link=p_link AND title=p_title 
+0

Đó là một ý tưởng thú vị, nhưng nó sẽ không phải là một nỗi đau để thực hiện một tham chiếu/khóa ngoài nếu khóa chính bao gồm nhiều lĩnh vực? Ngoài ra, lần đầu tiên tôi cố gắng tạo ra một ràng buộc duy nhất để đạt được kết quả tương tự, vì tôi cần phải trả lại đúng ID cũng về chèn lỗi do ràng buộc duy nhất mà tôi đã bỏ qua cách tiếp cận đó. Hơn nữa, tôi thấy nó là một rắc rối để đối phó với các cam kết khi đột nhiên có một thất bại do các hạn chế. – agnsaft

+1

@invictus Tại sao nó sẽ là một nỗi đau để thực hiện một tham chiếu/khóa ngoài nếu khóa chính bao gồm nhiều trường? Sẽ không có 'ID'. Bạn sẽ không phải trả lại bất cứ điều gì kể từ khi bạn đã biết những gì tự nhiên quan trọng (liên kết, tiêu đề) bạn đã cố gắng để chèn. –

4

Đây sẽ là một điều kiện chủng tộc tiềm năng trong PostgreSQL?

Có, và trên thực tế nó sẽ có trong bất kỳ công cụ cơ sở dữ liệu nào.

Cách tốt nhất để giải quyết vấn đề này để tránh các tình huống như vậy là gì?

Đây là câu hỏi được tải và sẽ yêu cầu kiến ​​thức thân mật về việc sử dụng cơ sở dữ liệu của nhiều người dùng. Tuy nhiên, tôi sẽ cung cấp cho bạn một số tùy chọn. Nói tóm lại, lựa chọn duy nhất bạn có là để LOCK bàn trong quá trình này, nhưng cách bạn khóa bảng mà sẽ phụ thuộc vào cách cơ sở dữ liệu được sử dụng trong suốt cả ngày.

Hãy bắt đầu với cơ LOCK:

LOCK TABLE feed_channel 

Đó sẽ khóa bảng bằng cách sử dụng tùy chọn ACCESS EXCLUSIVE khóa.

Xung đột với ổ khóa của tất cả các chế độ (SHARE ACCESS, ROW SHARE, ROW EXCLUSIVE, SHARE CẬP NHẬT ĐỘC QUYỀN, SHARE, SHARE ROW ĐỘC QUYỀN, ĐỘC QUYỀN, và truy cập độc quyền). Chế độ này đảm bảo rằng chủ sở hữu là giao dịch duy nhất truy cập vào bảng theo bất kỳ cách nào.

Bây giờ, đó là khóa hạn chế nhất hiện có và chắc chắn sẽ giải quyết tình trạng cuộc đua, nhưng có thể không chính xác như bạn muốn. Đó là điều bạn sẽ phải quyết định. Vì vậy, mặc dù nó là rõ ràng bạn sẽ phải LOCK bàn, nó không phải rõ ràng như thế nào.

Còn gì để quyết định?

  1. Làm thế nào nào bạn muốn LOCK the table? Nghiên cứu các tùy chọn khóa tại liên kết đó để đưa ra quyết định của bạn.
  2. Trường hợp bạn có muốn LOCK bảng không? Hay nói cách khác, bạn có muốn LOCK tại số đầu của hàm (mà tôi cho rằng bạn dựa trên điều kiện chủng tộc có thể) hoặc bạn chỉ muốn LOCK ngay trước INSERT?

Có thể thực hiện toàn bộ quy trình được lưu trữ nguyên tử, tức là chỉ có thể thực hiện một lần cùng một lúc?

Không, mã có thể được thực thi bởi bất kỳ ai được kết nối với cơ sở dữ liệu.


Tôi hy vọng điều này đã giúp bạn hướng dẫn.

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