Как создать функцию mug-request-safe postgresql для генерации различных последовательных чисел - PullRequest
0 голосов
/ 26 марта 2019

Мне нужно генерировать и хранить последовательные уникальные номера для каждой категории в контексте с несколькими возможными запросами одновременно.

Моя текущая база данных - 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;
...