Chiến lược đặt phòng nhóm đồng thời?


8

Hãy xem xét một cơ sở dữ liệu đặt chỗ. Có một danh sách n chỗ ngồi, và mỗi người có một thuộc tính is_booked. 0 có nghĩa là không, 1 có nghĩa là nó. Bất kỳ số lượng cao hơn và có một đặt trước quá nhiều.

Chiến lược để có nhiều giao dịch (trong đó mỗi giao dịch sẽ đặt một nhóm ghế y đồng thời) mà không cho phép đặt chỗ trước?

Tôi chỉ cần chọn tất cả các ghế chưa đặt, chọn một nhóm y được chọn ngẫu nhiên, đặt tất cả và kiểm tra xem đặt chỗ đó có đúng không (còn gọi là số is_booked không quá một, điều này biểu thị một giao dịch khác đã đặt chỗ và cam kết), sau đó cam kết. Nếu không thì hủy bỏ và thử lại.

Điều này được chạy ở mức cô lập Đọc Cam kết trong Postgres.

Câu trả lời:


5

Bởi vì bạn không nói cho chúng tôi nhiều về những gì bạn cần, tôi sẽ đoán mọi thứ và chúng tôi sẽ làm cho nó phức tạp vừa phải để đơn giản hóa một số câu hỏi có thể.

Điều đầu tiên về MVCC là trong một hệ thống đồng thời cao, bạn muốn tránh khóa bảng. Theo nguyên tắc chung, bạn không thể nói những gì không tồn tại mà không khóa bảng cho giao dịch. Điều đó cho bạn một lựa chọn: không dựa vào INSERT.

Tôi để lại rất ít như một bài tập cho một ứng dụng đặt phòng thực sự ở đây. Chúng tôi không xử lý,

  • Đặt trước vượt mức (như một tính năng)
  • Hoặc phải làm gì nếu không còn chỗ ngồi x.
  • Xây dựng cho khách hàng và giao dịch.

Chìa khóa ở đây là trong UPDATE.Chúng tôi chỉ khóa các hàng UPDATEtrước khi giao dịch bắt đầu. Chúng tôi có thể làm điều này bởi vì chúng tôi đã chèn tất cả vé ghế để bán trong bảng , event_venue_seats.

Tạo một lược đồ cơ bản

CREATE SCHEMA booking;
CREATE TABLE booking.venue (
  venueid    serial PRIMARY KEY,
  venue_name text   NOT NULL
  -- stuff
);
CREATE TABLE booking.seats (
  seatid        serial PRIMARY KEY,
  venueid       int    REFERENCES booking.venue,
  seatnum       int,
  special_notes text,
  UNIQUE (venueid, seatnum)
  --stuff
);
CREATE TABLE booking.event (
  eventid         serial     PRIMARY KEY,
  event_name      text,
  event_timestamp timestamp  NOT NULL
  --stuff
);
CREATE TABLE booking.event_venue_seats (
  eventid    int     REFERENCES booking.event,
  seatid     int     REFERENCES booking.seats,
  txnid      int,
  customerid int,
  PRIMARY KEY (eventid, seatid)
);

Kiểm tra dữ liệu

INSERT INTO booking.venue (venue_name)
VALUES ('Madison Square Garden');

INSERT INTO booking.seats (venueid, seatnum)
SELECT venueid, s
FROM booking.venue
  CROSS JOIN generate_series(1,42) AS s;

INSERT INTO booking.event (event_name, event_timestamp)
VALUES ('Evan Birthday Bash', now());

-- INSERT all the possible seat permutations for the first event
INSERT INTO booking.event_venue_seats (eventid,seatid)
SELECT eventid, seatid
FROM booking.seats
INNER JOIN booking.venue
  USING (venueid)
INNER JOIN booking.event
  ON (eventid = 1);

Và bây giờ cho giao dịch Đặt phòng

Bây giờ chúng tôi có mã hóa cứng cho một, bạn nên đặt nó thành bất kỳ sự kiện nào bạn muốn, customeridtxnidvề cơ bản làm cho chỗ ngồi được bảo lưu và cho bạn biết ai đã làm điều đó. Đây FOR UPDATElà chìa khóa. Những hàng này bị khóa trong quá trình cập nhật.

UPDATE booking.event_venue_seats
SET customerid = 1,
  txnid = 1
FROM (
  SELECT eventid, seatid
  FROM booking.event_venue_seats
  JOIN booking.seats
    USING (seatid)
  INNER JOIN booking.venue
    USING (venueid)
  INNER JOIN booking.event
    USING (eventid)
  WHERE txnid IS NULL
    AND customerid IS NULL
    -- for which event
    AND eventid = 1
  OFFSET 0 ROWS
  -- how many seats do you want? (they're all locked)
  FETCH NEXT 7 ROWS ONLY
  FOR UPDATE
) AS t
WHERE
  event_venue_seats.seatid = t.seatid
  AND event_venue_seats.eventid = t.eventid;

Cập nhật

Để đặt phòng theo thời gian

Bạn sẽ sử dụng một đặt phòng theo thời gian. Giống như khi bạn mua vé cho buổi hòa nhạc, bạn có M phút để xác nhận đặt phòng hoặc người khác có cơ hội - Neil McGuigan 19 phút trước

Những gì bạn sẽ làm ở đây là thiết lập booking.event_venue_seats.txnidnhư

txnid int REFERENCES transactions ON DELETE SET NULL

Người dùng thứ hai dự trữ seet, UPDATEđặt trong txnid. Bảng giao dịch của bạn trông giống như thế này.

CREATE TABLE transactions (
  txnid       serial PRIMARY KEY,
  txn_start   timestamp DEFAULT now(),
  txn_expire  timestamp DEFAULT now() + '5 minutes'
);

Sau đó, trong mỗi phút bạn chạy

DELETE FROM transactions
WHERE txn_expire < now()

Bạn có thể nhắc người dùng mở rộng bộ hẹn giờ khi gần hết hạn. Hoặc, chỉ cần để nó xóa txnidvà xếp tầng xuống để giải phóng chỗ ngồi.


Đây là một cách tiếp cận hay và thông minh: bảng giao dịch của bạn đóng vai trò khóa của bảng đặt chỗ thứ hai của tôi ; và có một sử dụng thêm.
joanolo

Trong phần "giao dịch đặt phòng", trong truy vấn phụ chọn bên trong của câu lệnh cập nhật, tại sao bạn tham gia chỗ ngồi, địa điểm và sự kiện vì bạn không sử dụng bất kỳ dữ liệu nào chưa được lưu trữ trong event_venue_seats?
Ynv

1

Tôi nghĩ rằng điều này có thể được thực hiện bằng cách sử dụng một bảng đôi nhỏ lạ mắt và một số ràng buộc.

Hãy bắt đầu bằng một số cấu trúc (không được chuẩn hóa hoàn toàn):

/* Everything goes to one schema... */
CREATE SCHEMA bookings ;
SET search_path = bookings ;

/* A table for theatre sessions (or events, or ...) */
CREATE TABLE sessions
(
    session_id integer /* serial */ PRIMARY KEY,
    session_theater TEXT NOT NULL,   /* Should be normalized */
    session_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    performance_name TEXT,           /* Should be normalized */
    UNIQUE (session_theater, session_timestamp) /* Alternate natural key */
) ;

/* And one for bookings */
CREATE TABLE bookings
(
    session_id INTEGER NOT NULL REFERENCES sessions (session_id),
    seat_number INTEGER NOT NULL /* REFERENCES ... */,
    booker TEXT NULL,
    PRIMARY KEY (session_id, seat_number),
    UNIQUE (session_id, seat_number, booker) /* Needed redundance */
) ;

Việc đặt bàn, thay vì có một is_bookedcột, đã có một bookercột. Nếu nó là null, chỗ ngồi không được đặt, nếu không đây là tên (id) của người đặt sách.

Chúng tôi thêm một số dữ liệu ví dụ ...

-- Sample data
INSERT INTO sessions 
    (session_id, session_theater, session_timestamp, performance_name)
VALUES 
    (1, 'Her Majesty''s Theatre', 
        '2017-01-06 19:30 Europe/London', 'The Phantom of the Opera'),
    (2, 'Her Majesty''s Theatre', 
        '2017-01-07 14:30 Europe/London', 'The Phantom of the Opera'),
    (3, 'Her Majesty''s Theatre', 
        '2017-01-07 19:30 Europe/London', 'The Phantom of the Opera') ;

-- ALl sessions have 100 free seats 
INSERT INTO bookings (session_id, seat_number)
SELECT
    session_id, seat_number
FROM
    generate_series(1, 3)   AS x(session_id),
    generate_series(1, 100) AS y(seat_number) ;

Chúng tôi tạo một bảng thứ hai để đặt chỗ, với một hạn chế:

CREATE TABLE bookings_with_bookers
(
    session_id INTEGER NOT NULL,
    seat_number INTEGER NOT NULL,
    booker TEXT NOT NULL,
    PRIMARY KEY (session_id, seat_number)
) ;

-- Restraint bookings_with_bookers: they must match bookings
ALTER TABLE bookings_with_bookers
  ADD FOREIGN KEY (session_id, seat_number, booker) 
  REFERENCES bookings.bookings (session_id, seat_number, booker) MATCH FULL
   ON UPDATE RESTRICT ON DELETE RESTRICT
   DEFERRABLE INITIALLY DEFERRED;

Bảng thứ hai này sẽ chứa một BẢN SAO của các bộ dữ liệu (session_id, seat_number, booker), với một FOREIGN KEYràng buộc; điều đó sẽ không cho phép các đặt phòng ban đầu được CẬP NHẬT bởi một nhiệm vụ khác. [Giả sử rằng không bao giờ có hai nhiệm vụ xử lý cùng một booker ; nếu đó là trường hợp, một task_idcột nhất định nên được thêm vào.]

Bất cứ khi nào chúng tôi cần thực hiện đặt phòng, chuỗi các bước tiếp theo trong chức năng sau đây sẽ hiển thị cách:

CREATE or REPLACE FUNCTION book_session 
    (IN _booker text, IN _session_id integer, IN _number_of_seats integer) 
RETURNS integer  /* number of seats really booked */ AS
$BODY$

DECLARE
    number_really_booked INTEGER ;
BEGIN
    -- Choose a random sample of seats, assign them to the booker.

    -- Take a list of free seats
    WITH free_seats AS
    (
    SELECT
        b.seat_number
    FROM
        bookings.bookings b
    WHERE
        b.session_id = _session_id
        AND b.booker IS NULL
    ORDER BY
        random()     /* In practice, you'd never do it */
    LIMIT
        _number_of_seats
    FOR UPDATE       /* We want to update those rows, and book them */
    )

    -- Update the 'bookings' table to have our _booker set in.
    , update_bookings AS 
    (
    UPDATE
        bookings.bookings b
    SET
        booker = _booker
    FROM
        free_seats
    WHERE
        b.session_id  = _session_id AND 
        b.seat_number = free_seats.seat_number
    RETURNING
        b.session_id, b.seat_number, b.booker
    )

    -- Insert all this information in our second table, 
    -- that acts as a 'lock'
    , insert_into_bookings_with_bookers AS
    (
    INSERT INTO
        bookings.bookings_with_bookers (session_id, seat_number, booker)
    SELECT
        update_bookings.session_id, 
        update_bookings.seat_number, 
        update_bookings.booker
    FROM
        update_bookings
    RETURNING
        bookings.bookings_with_bookers.seat_number
    )

    -- Count real number of seats booked, and return it
    SELECT 
        count(seat_number) 
    INTO
        number_really_booked
    FROM
        insert_into_bookings_with_bookers ;

    RETURN number_really_booked ;
END ;
$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF STRICT
COST 10000 ;

Để thực sự đặt chỗ, chương trình của bạn nên cố gắng thực hiện một cái gì đó như:

-- Whenever we wich to book 37 seats for session 2...
BEGIN TRANSACTION  ;
SELECT
    book_session('Andrew the Theater-goer', 2, 37) ;

/* Three things can happen:
    - The select returns the wished number of seats  
         => COMMIT 
           This can cause an EXCEPTION, and a need for (implicit)
           ROLLBACK which should be handled and the process 
           retried a number of times
           if no exception => the process is finished, you have your booking
    - The select returns less than the wished number of seats
         => ROLLBACK and RETRY
           we don't have enough seats, or some rows changed during function
           execution
    - (There can be a deadlock condition... that should be handled)
*/
COMMIT /* or ROLLBACK */ TRANSACTION ;

Điều này phụ thuộc vào hai sự kiện 1. FOREIGN KEYRàng buộc sẽ không cho phép dữ liệu bị phá vỡ . 2. Chúng tôi CẬP NHẬT bảng đặt chỗ, nhưng chỉ CHERTN (và không bao giờ CẬP NHẬT ) trên bảng đặt phòng_with_bookers một (bảng thứ hai).

Nó không cần SERIALIZABLEmức cô lập, điều này sẽ đơn giản hóa rất nhiều logic. Tuy nhiên, trong thực tế, các khóa chết sẽ được dự kiến ​​và chương trình tương tác với cơ sở dữ liệu nên được thiết kế để xử lý chúng.


Nó không cần SERIALIZABLEbởi vì nếu hai book_session được thực thi cùng một lúc thì txn count(*)thứ hai có thể đọc bảng trước khi book_session đầu tiên được thực hiện với nó INSERT. Theo nguyên tắc chung, sẽ không an toàn khi kiểm tra wo / không tồn tại SERIALIZABLE.
Evan Carroll

@EvanCarroll: Tôi nghĩ rằng sự kết hợp của 2 bảng và sử dụng CTE sẽ tránh được sự cần thiết này. Bạn chơi với thực tế là các ràng buộc cung cấp cho bạn một đảm bảo rằng, vào cuối giao dịch của bạn, mọi thứ đều nhất quán hoặc bạn hủy bỏ. Nó hành xử theo một cách rất giống với tuần tự hóa .
joanolo

1

Tôi sẽ sử dụng một CHECKràng buộc để ngăn chặn đặt trước quá mức và tránh khóa hàng rõ ràng.

Bảng có thể được định nghĩa như thế này:

CREATE TABLE seats
(
    id serial PRIMARY KEY,
    is_booked int NOT NULL,
    extra_info text NOT NULL,
    CONSTRAINT check_overbooking CHECK (is_booked >= 0 AND is_booked <= 1)
);

Việc đặt một lô ghế được thực hiện bởi một lần duy nhất UPDATE:

UPDATE seats
SET is_booked = is_booked + 1
WHERE 
    id IN
    (
        SELECT s2.id
        FROM seats AS s2
        WHERE
            s2.is_booked = 0
        ORDER BY random() -- or id, or some other order to choose seats
        LIMIT <number of seats to book>
    )
;
-- in practice use RETURNING to get back a list of booked seats,
-- or prepare the list of seat ids which you'll try to book
-- in a separate step before this UPDATE, not on the fly like here.

Mã của bạn nên có logic thử lại. Thông thường, chỉ cần cố gắng để chạy này UPDATE. Giao dịch sẽ bao gồm một trong những này UPDATE. Nếu không có vấn đề gì, bạn có thể chắc chắn rằng toàn bộ lô đã được đặt. Nếu bạn nhận được một vi phạm ràng buộc KIỂM TRA, bạn nên thử lại.

Vì vậy, đây là một cách tiếp cận lạc quan.

  • Đừng khóa bất cứ điều gì rõ ràng.
  • Cố gắng thực hiện thay đổi.
  • Thử lại nếu ràng buộc bị vi phạm.
  • Bạn không cần bất kỳ kiểm tra rõ ràng nào sau UPDATE, bởi vì ràng buộc (tức là công cụ DB) thực hiện nó cho bạn.

1

Cách tiếp cận 1s - CẬP NHẬT duy nhất:

UPDATE seats
SET is_booked = is_booked + 1
WHERE seat_id IN
(SELECT seat_id FROM seats WHERE is_booked = 0 LIMIT y);

Cách tiếp cận thứ 2 - LOOP (plpgsql):

v_counter:= 0;
WHILE v_counter < y LOOP
  SELECT seat_id INTO STRICT v_seat_id FROM seats WHERE is_booked = 0 LIMIT 1;
  UPDATE seats SET is_booked = 1 WHERE seat_id = v_seat_id AND is_booked = 0;
  GET DIAGNOSTICS v_rowcount = ROW_COUNT;
  IF v_rowcount > 0 THEN v_counter:= v_counter + 1; END IF;
END LOOP;

Cách tiếp cận thứ 3 - Bảng xếp hàng:

Bản thân các giao dịch không cập nhật bảng chỗ ngồi. Tất cả họ CHỌN yêu cầu của họ vào một bảng xếp hàng.
Một quy trình riêng biệt lấy tất cả các yêu cầu từ bảng xếp hàng và xử lý chúng, bằng cách phân bổ chỗ ngồi cho người yêu cầu.

Ưu điểm:
- Bằng cách sử dụng INSERT, khóa / tranh chấp được loại bỏ
- Không đảm bảo đặt trước vượt mức bằng cách sử dụng một quy trình duy nhất để phân bổ chỗ ngồi

Nhược điểm:
- Phân bổ chỗ ngồi không ngay lập tức

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.