Обеспечение свободного ограничения в postgres между несколькими клиентами

У меня есть таблица со следующими полями:

entry_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
uuid VARCHAR(256) NOT NULL,
session_start TIMESTAMP NOT NULL,
session_end TIMESTAMP NOT NULL,
user_ip VARCHAR(40) NOT NULL,
user_agent VARCHAR(256) NOT NULL,

Теперь у меня много входящих запросов с кортежами данных типа (site_id, uuid, timestamp, user_ip, user_agent).

Мое правило состоит в том, что если в базе данных есть запись, возраст которой менее 3 часов (session_end), то входящий запрос обновляет session_end = timestamp. Если нет, создайте новую запись (где session_start = session_end = timestamp).

Входящие запросы обрабатываются несколькими процессами. Скажем, 3-4 входящих запроса попадают на мои серверы с одинаковыми данными (разные временные метки, но в миллисекундах) и обрабатываются 3 разными процессами - как мне избежать создания 3 разных записей (если все они проверяют одновременно, см. нет совпадений записей и каждая создает новую)? Это вопрос состояния гонки, и я не знаю, как его обеспечить.

Блокировка таблицы кажется излишней, поскольку это таблица с большим объемом записи, но какие у меня есть альтернативы помимо механизма блокировки стороннего производителя?

Пример:

Format:
(site_id, uuid, timestamp, user_ip, user_agent)

Incoming requests / data:
(1, 123, 2014-01-01T10:00:32, '123.123.123.123', 'Mozilla/Chrome')
(1, 123, 2014-01-01T10:00:33, '123.123.123.123', 'Mozilla/Chrome')
(1, 123, 2014-01-01T10:00:34, '123.123.123.123', 'Mozilla/Chrome')

Result tuple:
entry_id | site_id | uuid | session_start       | session_end         | user_ip | user_agent
--------------------------------------------------------------------------------------------
<auto>   |       1 |  123 | 2014-01-01T10:00:32 | 2014-01-01T10:00:34 | ...     | ...

person Christian P.    schedule 02.09.2014    source источник
comment
Как насчет UNIQUE индекса и обработки ошибок вставки в приложениях?   -  person Jakub Kania    schedule 02.09.2014
comment
Как бы вы сделали УНИКАЛЬНЫЙ выписку за период времени? (site_id, uuid, user_ip, user_agent) могут быть установлены уникальными вместе, но несколько строк разрешены, если никакие две временные метки session_start / session_end не находятся в пределах 3 часов друг от друга.   -  person Christian P.    schedule 02.09.2014
comment
Похоже, эти временные метки - естественные ключи.   -  person supertopi    schedule 02.09.2014
comment
Добавлен пример, чтобы прояснить мою точку зрения   -  person Christian P.    schedule 02.09.2014
comment
Версия? select version();   -  person Clodoaldo Neto    schedule 02.09.2014
comment
возможный дубликат ограничения перекрытия даты Postgres   -  person Jakub Kania    schedule 02.09.2014
comment
@ChristianP О, в этом случае вы можете использовать ограничение exclude, на самом деле уже есть почти точно такой же вопрос по SO, вам просто нужно изменить часть диапазона: stackoverflow.com/q/10616099/2115135   -  person Jakub Kania    schedule 02.09.2014


Ответы (2)


Создайте gist ограничение исключения с типом timestamp range

create table request (
    entry_id bigserial primary key,
    site_id bigint not null,
    uuid varchar(256) not null,
    session_start timestamp not null,
    session_end timestamp not null,
    user_ip varchar(40) not null,
    user_agent varchar(256) not null,
    constraint session_overlap exclude using gist (
        site_id with =,
        uuid with =,
        user_ip with =,
        user_agent with =,
        tsrange(session_end, session_end + interval '3 hours', '[)') with &&
    )
);

Теперь эта вставка не работает:

insert into request (site_id, uuid, session_start, session_end, user_ip, user_agent)
select site_id, uuid, ts::timestamp, ts::timestamp, user_id, user_agent
from (values
    (1, '123', '2014-01-01T10:00:32', '123.123.123.123', 'Mozilla/Chrome'),
    (1, '123', '2014-01-01T10:00:33', '123.123.123.123', 'Mozilla/Chrome'),
    (1, '123', '2014-01-01T10:00:34', '123.123.123.123', 'Mozilla/Chrome')
) s(site_id, uuid, ts, user_id, user_agent)
;
ERROR:  conflicting key value violates exclusion constraint "session_overlap"
DETAIL:  Key (site_id, uuid, user_ip, user_agent, tsrange(session_end, session_end + '03:00:00'::interval, '[)'::text))=(1, 123, 123.123.123.123, Mozilla/Chrome, ["2014-01-01 10:00:33","2014-01-01 13:00:33")) conflicts with existing key (site_id, uuid, user_ip, user_agent, tsrange(session_end, session_end + '03:00:00'::interval, '[)'::text))=(1, 123, 123.123.123.123, Mozilla/Chrome, ["2014-01-01 10:00:32","2014-01-01 13:00:32")).

Возможно, вам потребуется установить расширение btree_gist как суперпользователь

create extension btree_gist;

http://www.postgresql.org/docs/current/interactive/btree-gist.html

person Clodoaldo Neto    schedule 02.09.2014
comment
Хорошее предложение, но для простоты реализации и более легкого изменения ограничений (скажем, от 3 до 5 часов) я выберу рекомендательное решение блокировки. Я не знал об ограничениях исключения сущности, поэтому спасибо за то, что научил меня этому :) - person Christian P.; 02.09.2014
comment
@Christian: Я не понимаю. Первый процесс замков и вставок. Затем другой процесс ожидает снятия блокировки и вставляет кортеж, нарушающий авторские права. Разве это не то, чего вы хотите избежать? Что вы получаете, просто ожидая? - person Clodoaldo Neto; 02.09.2014
comment
Я провожу несколько других проверок, поэтому я в основном удерживаю блокировку до запуска всего конвейера, а затем освобождаю ее в конце, предотвращая неправильную вставку любых кортежей. Следствие, что быстрее - попытка и наличие ошибки ограничения или проверка, а затем вставка? - person Christian P.; 03.09.2014
comment
@Christian: Ограничение - это хорошая практика, которую вы проверяете перед вставкой или нет. Насколько я понимаю из вашего вопроса, вы делаете аперт и всегда будете проверять. На мой взгляд, странность здесь в том, что блокировка не является ограничением, но, опять же, должно быть ограничение, блокируете вы или нет. За свой долгий опыт я никогда не встречал случая, когда блокировка была бы лучше или полезнее по сравнению с очень простым try/catch обеспечением на стороне приложения. - person Clodoaldo Neto; 03.09.2014

Ознакомьтесь с рекомендациями по блокировкам.

SELECT pg_advisory_lock(key);
// INSERT OR UPDATE...
SELECT pg_advisory_unlock(key);

Или используя версию с блокировкой nob:

SELECT pg_try_advisory_lock(key) INTO :acquired;
// if (acquired) then INSERT OR UPDATE...
SELECT pg_advisory_unlock(key); 
person Vlad    schedule 02.09.2014
comment
Ах, это позволило бы мне блокировать только небольшую часть запросов за раз, это было бы идеально! - person Christian P.; 02.09.2014
comment
@ChristianP. да, здесь у вас есть гибкость, так как вы можете определить свой собственный ключ, который может охватывать множество наборов, которые вы хотите. - person Vlad; 02.09.2014
comment
Точно. Я, вероятно, просто добавлю разные поля вместе, исключая временную метку, и заблокирую это (вычисление a. Шаблон таков, что первые 2-4 запроса приходят сразу после друг друга, а затем они разлетаются, что делает это менее проблемным. Ура! - person Christian P.; 02.09.2014
comment
@ChristianP. Просто понял, что неблокирующий pg_try_advisory_lock подойдет еще лучше. - person Vlad; 02.09.2014
comment
Я запускаю это внутри транзакции и хочу, чтобы она была заблокирована, поэтому я буду использовать pg_advisory_xact_lock. Но спасибо за внимание :) - person Christian P.; 03.09.2014