Мне нужно генерировать и хранить последовательные уникальные номера для каждой категории в контексте с несколькими возможными запросами одновременно.
Моя текущая база данных - Postgresql, и моя идея состоит в том, чтобы иметь таблицу для хранения этих чисел и использовать функцию для их генерации.
Функция (она же хранимая процедура) отвечает за генерацию нового номера и обеспечивает (через директиву LOCK) его уникальность для каждой категории.
Я думал об использовании директивы «LOCK», но обнаружил странное поведение: если я использую «ACCESS EXCLUSIVE MODE», я получаю больше конфликтов, чем при использовании «SHARE ROW EXCLUSIVE»: для меня это бессмысленно, потому что «ACCESS» «ЭКСКЛЮЗИВ» должен блокировать всю таблицу на все время работы функции ВСЕМ возможным способом (по крайней мере, это мое понимание из официальной документации ..).
Я попробовал это решение с Postgresql 10.4 и протестировал его с помощью теста c # Xunit, который делает цикл и для каждой итерации моделирует «100» одновременных соединений и запрашивает генерацию уникального номера (вызов функции).
Если я использую «ACCESS EXCLUSIVE MODE», после некоторой итерации (правильно сгенерированные числа 100/200) я получаю исключение, заданное уникальным индексом.
Если я использую «SHARE ROW EXCLUSIVE», после 58 итераций (правильно сгенерированных 580 номеров) я получаю исключение.
Почему?
Скрипт для создания таблицы:
CREATE TABLE IF NOT EXISTS protocol
(
id UUID NOT NULL PRIMARY KEY,
code BIGINT default 0 NOT NULL,
registry__id UUID NOT NULL,
registry__name varchar(255) NOT NULL,
what varchar(255) NULL,
createdat timestamp not null,
createdby varchar(255)
);
-- cannot use unique constraint because works only with columns
-- with an index i can add the unique constraint that I want
-- note: this constraint does not check if there are 2 equal code with different year!
create unique index protocol_per_year on "protocol" (
code,
registry__id,
extract(year from createdat)
);
Функция:
DROP FUNCTION IF EXISTS createNewProtocol(uuid, varchar, varchar, uuid, timestamp);
CREATE OR REPLACE FUNCTION createNewProtocol(in registryId uuid,
in what varchar,
in createdby varchar,
in protocolId uuid,
in createdAtValue timestamp) RETURNS bigint AS
$$
DECLARE
registryName VARCHAR := null;
startFromVal bigint := null;
codeVal bigint := null;
lastCode bigint := null;
resetByYear boolean := false;
BEGIN
LOCK TABLE "protocol" IN SHARE ROW EXCLUSIVE MODE;
--LOCK TABLE "protocol" IN ACCESS EXCLUSIVE MODE;
SELECT p.name, p.startfrom, p.reseteveryyear INTO STRICT registryName, startFromVal, resetByYear
from "protocolregistry" p
where p.id = registryId;
IF registryName IS NULL || registryName = '' THEN
RAISE EXCEPTION 'Can not find protocol registry with id %', registryId;
end if;
IF resetByYear THEN
lastCode := (SELECT p.code
from "protocol" p
where p.registry__id = registryId
AND EXTRACT(year from p.createdat) = EXTRACT(year from createdAtValue)
ORDER BY p.createdat DESC
LIMIT 1);
ELSE
lastCode := (SELECT p.code from "protocol" p where p.registry__id = registryId ORDER BY p.createdat DESC LIMIT 1);
END IF;
codeVal = 0;
if lastCode IS NULL then
codeVal = startFromVal;
ELSE
codeVal = lastCode + 1;
end if;
INSERT INTO "protocol" (id, code, registry__id, registry__name, what, createdat, createdby)
VALUES (protocolId, codeVal, registryId, registryName, what, createdAtValue, createdby);
RETURN codeVal;
END;
$$ LANGUAGE plpgsql;