我认为这可以通过使用一些花哨的双表和一些约束来实现。
让我们从一些(未完全标准化)的结构开始:
/* 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 */
) ;
表格预订没有一is_booked
栏,而是一booker
栏。如果为空,则不预订座位,否则为预订者的姓名(id)。
我们添加一些示例数据...
-- 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) ;
我们创建第二张预订表,但有一个限制:
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;
第二个表将包含(session_id,seat_number,booker)元组的COPY,并具有一个FOREIGN KEY
约束;这会不会让原本预定由另一个任务进行更新。[假设同一个预订者永远不会有两个任务;如果是这种情况,task_id
则应添加特定的列。]
每当我们需要进行预订时,以下功能中遵循的步骤顺序就会显示出以下方式:
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 ;
要真正进行预订,您的程序应尝试执行以下操作:
-- 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 ;
这取决于两个事实:1. FOREIGN KEY
约束不允许破坏数据。2.我们更新了bookings表,但是对bookings_with_bookers的一个表(第二个表)仅进行了INSERT(并且从未进行UPDATE)。
它不需要SERIALIZABLE
隔离级别,这将大大简化逻辑。但是,实际上,死锁是可以预料的,与数据库交互的程序应设计为处理死锁。