函数内的 Postgres 咨询锁允许并发执行
Postgres advisory lock within function allows concurrent execution
我遇到了一个问题,我有一个函数需要根据某些情况进行序列化访问。这似乎是使用咨询锁的好案例。但是,在相当重的负载下,我发现没有发生序列化访问,并且我看到了对该函数的并发访问。
此函数的目的是为事件提供"inventory control"。意思是,它旨在限制给定事件的并发购票,以便该事件不会超卖。这些是 application/database 中唯一使用的咨询锁。
我发现有时活动中的门票数量超过 eventTicketMax 值。由于咨询锁,这似乎不可能。当以低容量测试时(或在获取锁后手动引入延迟,例如 pg_sleep),一切按预期工作。
CREATE OR REPLACE FUNCTION createTicket(
userId int,
eventId int,
eventTicketMax int
) RETURNS integer AS $$
DECLARE insertedId int;
DECLARE numTickets int;
BEGIN
-- first get the event lock
PERFORM pg_advisory_lock(eventId);
-- make sure we aren't over ticket max
numTickets := (SELECT count(*) FROM api_ticket
WHERE event_id = eventId and status <> 'x');
IF numTickets >= eventTicketMax THEN
-- raise an exception if this puts us over the max
-- and bail
PERFORM pg_advisory_unlock(eventId);
RAISE EXCEPTION 'Maximum entries number for this event has been reached.';
END IF;
-- create the ticket
INSERT INTO api_ticket (
user_id,
event_id,
created_ts
)
VALUES (
userId,
eventId,
now()
)
RETURNING id INTO insertedId;
-- update the ticket count
UPDATE api_event SET ticket_count = numTickets + 1 WHERE id = eventId;
-- release the event lock
PERFORM pg_advisory_unlock(eventId);
RETURN insertedId;
END;
$$ LANGUAGE plpgsql;
这是我的环境设置:
- Django 1.8.1(django.db.backends.postgresql_psycopg2 w/ CONN_MAX_AGE 300)
- PGBouncer 1.7.2(session 模式)
- Amazon RDS 上的 Postgres 9.3.10
我尝试调整的其他变量:
- 将 CONN_MAX_AGE 设置为 0
- 删除 pgbouncer 并直接连接到 DB
在我的测试中,我注意到,在活动超卖的情况下,门票是从不同的网络服务器购买的,所以我认为共享 session 没有任何有趣的事情,但我不能肯定。
一旦 PERFORM pg_advisory_unlock(eventId)
执行,另一个会话可以获取该锁,但由于会话 #1 的 INSERT 尚未提交,因此不会被计算在 COUNT(*)
of 会话中#2,导致超额预订。
如果保留建议锁策略,则必须使用事务级建议锁 (pg_advisory_xact_lock
),而不是会话级。这些锁会在 COMMIT 时自动释放。
我遇到了一个问题,我有一个函数需要根据某些情况进行序列化访问。这似乎是使用咨询锁的好案例。但是,在相当重的负载下,我发现没有发生序列化访问,并且我看到了对该函数的并发访问。
此函数的目的是为事件提供"inventory control"。意思是,它旨在限制给定事件的并发购票,以便该事件不会超卖。这些是 application/database 中唯一使用的咨询锁。
我发现有时活动中的门票数量超过 eventTicketMax 值。由于咨询锁,这似乎不可能。当以低容量测试时(或在获取锁后手动引入延迟,例如 pg_sleep),一切按预期工作。
CREATE OR REPLACE FUNCTION createTicket(
userId int,
eventId int,
eventTicketMax int
) RETURNS integer AS $$
DECLARE insertedId int;
DECLARE numTickets int;
BEGIN
-- first get the event lock
PERFORM pg_advisory_lock(eventId);
-- make sure we aren't over ticket max
numTickets := (SELECT count(*) FROM api_ticket
WHERE event_id = eventId and status <> 'x');
IF numTickets >= eventTicketMax THEN
-- raise an exception if this puts us over the max
-- and bail
PERFORM pg_advisory_unlock(eventId);
RAISE EXCEPTION 'Maximum entries number for this event has been reached.';
END IF;
-- create the ticket
INSERT INTO api_ticket (
user_id,
event_id,
created_ts
)
VALUES (
userId,
eventId,
now()
)
RETURNING id INTO insertedId;
-- update the ticket count
UPDATE api_event SET ticket_count = numTickets + 1 WHERE id = eventId;
-- release the event lock
PERFORM pg_advisory_unlock(eventId);
RETURN insertedId;
END;
$$ LANGUAGE plpgsql;
这是我的环境设置:
- Django 1.8.1(django.db.backends.postgresql_psycopg2 w/ CONN_MAX_AGE 300)
- PGBouncer 1.7.2(session 模式)
- Amazon RDS 上的 Postgres 9.3.10
我尝试调整的其他变量:
- 将 CONN_MAX_AGE 设置为 0
- 删除 pgbouncer 并直接连接到 DB
在我的测试中,我注意到,在活动超卖的情况下,门票是从不同的网络服务器购买的,所以我认为共享 session 没有任何有趣的事情,但我不能肯定。
一旦 PERFORM pg_advisory_unlock(eventId)
执行,另一个会话可以获取该锁,但由于会话 #1 的 INSERT 尚未提交,因此不会被计算在 COUNT(*)
of 会话中#2,导致超额预订。
如果保留建议锁策略,则必须使用事务级建议锁 (pg_advisory_xact_lock
),而不是会话级。这些锁会在 COMMIT 时自动释放。