Предпосылки
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
Конечно, это должно быть сделано в параллельном вопросе. Теперь я прочитал/попробовал разные решения.
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. Не уверен, чего ожидать
Второе решение, которое я пробовал, состояло в том, чтобы добавить PESIMISTIC_WRITE к методу findById сущности ProductsLocationCounter, но в итоге я получил
cannot execute SELECT FOR UPDATE in a read-only transaction хотя я выполнял код в аннотированном методе @Transactional (который по умолчанию имеет значение только для чтения false).
Третий заключался в обновлении и извлечении значения счетчика в том же операторе, но Spring jpa не позволяет этого (и базового db), поскольку оператор обновления возвращает только количество затронутых строк.
Есть ли другое решение или мне нужно что-то добавить в функцию триггера, чтобы сделать ее потокобезопасной? Спасибо
почему 3 триггера?
Если я правильно понимаю, вам нужно синхронизировать значение products.counter_value с product_location_count.counter, поэтому вам нужны три триггера: один для вставки, один для удаления и один для обновления.
Нет, мне не нужна синхронизация. Мне нужно получить атомарное значение product_location_count.counter в момент времени t1, добавить значение к продукту, обновить product_location_count.counter = product_location_count.counter + 1, а затем освободить его, чтобы другой поток/клиент забрал следующее значение.
Прежде чем углубляться в технические детали, я бы посоветовал подумать, для чего нужны эти счетчики с точки зрения бизнеса. Не могли бы вы обновить пост, пожалуйста, и предоставить более подробную информацию? Для чего используется product.counter?
спасибо @AndriySlobodyanyk, но я уже объяснил бизнес-логику. Я нашел решение в любом случае.
Хочу только подчеркнуть, что проблема выглядит как типичный счетчик. Это хорошо, но, с другой стороны, это является узким местом, все запросы на создание продукта могут обрабатываться только последовательно, не могут масштабироваться горизонтально и так далее.
Вот в чем проблема. Поскольку API имеет несколько экземпляров, подключенных к базе данных, мы не можем сделать этот серийный номер, потому что он не применяется в этом случае. Это типичный случай счетчика в распределенной среде, но я в основном хотел, чтобы БД справилась с работой. Кроме того, учитывая, что это всего лишь счетчик, операции по выборке и обновлению будут выполняться довольно быстро, поэтому, даже если это узкое место, я принимаю его как есть.




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