函数内的Postgres咨询锁允许并发执行

Bro*_*ood 6 django postgresql concurrency pgbouncer

我遇到了一个问题,我有一个功能,根据某些情况需要序列化访问.这似乎是使用咨询锁的好例子.但是,在相当繁重的负载下,我发现序列化访问没有发生,我看到并发访问该函数.

该功能的目的是为事件提供"库存控制".这意味着,它旨在限制特定事件的并发票购买,以使事件不被超卖.这些是应用程序/数据库中使用的唯一建议锁.

我发现偶尔会有比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;
Run Code Online (Sandbox Code Playgroud)

这是我的环境设置:

  • Django 1.8.1(django.db.backends.postgresql_psycopg2 w/CONN_MAX_AGE 300)
  • PGBouncer 1.7.2(会话模式)
  • 在Amazon RDS上发布Postgres 9.3.10

我尝试调整的其他变量:

  • 将CONN_MAX_AGE设置为0
  • 删除pgbouncer并直接连接到DB

在我的测试中,我注意到,在事件被超卖的情况下,门票是从不同的网络服务器购买的,所以我认为关于共享会话没有任何有趣的业务,但我不能肯定地说.

Dan*_*ité 5

一旦PERFORM pg_advisory_unlock(eventId)执行,另一个会话即可获取该锁,但是由于尚未提交会话#1的INSERT,因此不会将其计入COUNT(*)会话#2中,从而导致预订过多。

如果保留咨询锁定策略,则应使用事务级别的咨询锁定(pg_advisory_xact_lock),而不是会话级别。这些锁在COMMIT时自动释放。

另请注意,事务隔离级别很重要。默认情况下,postgres使用,REPEATABLE READ并且乍一看似乎Django没有脱离它。重要的是COUNT(*)要反映上一个提交的状态,而不是事务开始时的状态。