Параллельный столбец счетчика postgresql JPA с извлекаемым значением

Предпосылки

PostgreSQL

Весенняя загрузка с весенними данными jpa

Проблема

У меня 2 стола. Products и ProductsLocationCounter. У каждого продукта есть поля location_id и counter_value среди прочих. location_id также является первичным ключом ProductsLocationCounter. ProductsLocationCounter предназначен для ведения счетчика количества продуктов, сгруппированных по определенному location_id, всякий раз, когда добавляется новый продукт. Проблема в том, что мне также нужно, чтобы значение счетчика в этот момент времени было привязано к объекту продукта.

Таким образом, поток будет похож на

 1. create product
 2. counter_value = get counter
 3. increment counter
 4. product.counter_value = counter_value

Конечно, это должно быть сделано в параллельном вопросе. Теперь я прочитал/попробовал разные решения.

  1. этот пост stackoverflow предполагает, что я должен позволить базе данных обрабатывать параллелизм, что мне кажется вполне приемлемым. Но фишка в том, что мне нужно значение счетчика в той же транзакции. Итак, я создал триггер
CREATE FUNCTION maintain_location_product_count_fun() RETURNS TRIGGER AS
$$
DECLARE
counter_var BIGINT;
BEGIN
  IF TG_OP IN ('INSERT') THEN
    select product_location_count.counter into counter_var from product_location_count WHERE id = new.location_id FOR UPDATE;
    UPDATE product_location_count SET counter = counter + 1 WHERE id = new.location_id;
    UPDATE products SET counter_value = counter_var WHERE location_id = new.location_id;
  END IF;
  RETURN NULL;
END
$$
LANGUAGE plpgsql;

CREATE TRIGGER maintain_location_product_count_trig
AFTER INSERT ON products
FOR EACH ROW
EXECUTE PROCEDURE maintain_location_product_count_fun();

и протестировал его с параллельным потоком

    IntStream.range(1, 5000)
        .parallel()
        .forEach(value -> {
          executeInsideTransactionTemplate(status -> {
            var location = locationRepository.findById(location.getId()).get();
            return addProductWithLocation(location)
          });
        });

Дублирования в столбце counter_value нет. Безопасен ли этот триггер для многопоточных приложений? Раньше не работал с триггерами/функциями postgresql. Не уверен, чего ожидать


  1. Второе решение, которое я пробовал, состояло в том, чтобы добавить PESIMISTIC_WRITE к методу findById сущности ProductsLocationCounter, но в итоге я получил cannot execute SELECT FOR UPDATE in a read-only transaction хотя я выполнял код в аннотированном методе @Transactional (который по умолчанию имеет значение только для чтения false).

  2. Третий заключался в обновлении и извлечении значения счетчика в том же операторе, но Spring jpa не позволяет этого (и базового db), поскольку оператор обновления возвращает только количество затронутых строк.

Есть ли другое решение или мне нужно что-то добавить в функцию триггера, чтобы сделать ее потокобезопасной? Спасибо

триггер кажется в порядке (я бы предпочел удалить первый выбор ... для обновления в пользу возврата в обновление stmt. хотя, и вам действительно нужно создать три триггера), однако, если таблица product_location_count управляется HBN, и это не помечен как @Immutable (или столбец счетчика не помечен как необновляемый), вы можете потерять некоторые обновления

Andrey B. Panfilov 25.10.2022 13:28

почему 3 триггера?

ALex 25.10.2022 13:39

Если я правильно понимаю, вам нужно синхронизировать значение products.counter_value с product_location_count.counter, поэтому вам нужны три триггера: один для вставки, один для удаления и один для обновления.

Andrey B. Panfilov 25.10.2022 13:42

Нет, мне не нужна синхронизация. Мне нужно получить атомарное значение product_location_count.counter в момент времени t1, добавить значение к продукту, обновить product_location_count.counter = product_location_count.counter + 1, а затем освободить его, чтобы другой поток/клиент забрал следующее значение.

ALex 25.10.2022 13:59

Прежде чем углубляться в технические детали, я бы посоветовал подумать, для чего нужны эти счетчики с точки зрения бизнеса. Не могли бы вы обновить пост, пожалуйста, и предоставить более подробную информацию? Для чего используется product.counter?

Andriy Slobodyanyk 25.10.2022 16:25

спасибо @AndriySlobodyanyk, но я уже объяснил бизнес-логику. Я нашел решение в любом случае.

ALex 26.10.2022 14:51

Хочу только подчеркнуть, что проблема выглядит как типичный счетчик. Это хорошо, но, с другой стороны, это является узким местом, все запросы на создание продукта могут обрабатываться только последовательно, не могут масштабироваться горизонтально и так далее.

Andriy Slobodyanyk 26.10.2022 16:56

Вот в чем проблема. Поскольку API имеет несколько экземпляров, подключенных к базе данных, мы не можем сделать этот серийный номер, потому что он не применяется в этом случае. Это типичный случай счетчика в распределенной среде, но я в основном хотел, чтобы БД справилась с работой. Кроме того, учитывая, что это всего лишь счетчик, операции по выборке и обновлению будут выполняться довольно быстро, поэтому, даже если это узкое место, я принимаю его как есть.

ALex 27.10.2022 09:18
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
8
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вот так я добился того, что мне было нужно.

Короче говоря, я использовал функцию sql и назвал ее внутри репозитория. Мне больше не нужен был спусковой крючок.

https://stackoverflow.com/a/74208072/3018285

Другие вопросы по теме