Jpa metoda robiąca saveOrUpdate, duża ilość requestów w krótkim czasie

0

Hej,

Mam wystawiony endpoint, który przyjmuje jakieś dane, i jeśli znajdzie w bazie pasujący rekord to robi update, w innym przypadku zapisuje nowy rekord. ( nie po id, tylko po innym zwykłym polu )

Czyli mniej więcej logika pseudokodu wygląda tak:

    @Transactional
	public void saveOrUpdateOrder(OrderDto dto) {
		orderRepository.findByNumber(dto.getSomeNumber).ifPresentOrElse(
				order -> updateOrder(order, dto),
				() -> createNewOrder(dto))
		);
	}

	private void updateOrder(Order order, OrderDto dto) {
			order.setSomething(dto.getSomething);
			order.setSomethingElse(dto.getSomethingElse());
			orderRepository.saveAndFlush(order);
	}

	private void createOrder(OrderDto dto) {
		Order newOrder = new Order(dto.getSomething(), dto.getSomethingElse());
		orderRepository.saveAndFlush(newOrder);
	}

No ale teraz problem jest taki, że mogę dostać w bardzo krótkim czasie dużo requestów (różnice milisekundowe). I wtedy działa to tak, że do bazy zostaje zapisanych pare rekordów o tym samym 'someNumber', aż w pewnym momencie zaczyna lecieć błąd, że query did not return a unique result( powinien zapisać się pierwszy rekord, a następne robić na nim update )

Macie jakiś pomysł, jak wstrzymać kolejne requesty, tak, żeby najpierw został zapisany w bazie rekord, a później kolejne już robiły update?

4

Jaki masz poziom izolacji transakcji? Wygląda że repeatable reads a chcesz serializable. Ale to dość "ciężkie" rozwiazanie, porównywalne z synchronizowaniem tego twojego repozytorium.
Lżejsze rozwiązanie to zrobić jakieś eventual consistency, stan trzymać w pamięci synchronizować go z bazą danych co jakiś czas, niezależnie od requestów. A same requesty byłyby obsługiwane w pamięci z jakimś Caffeine cache, dzięki czemu można by to sobie synchronizować rzędy wielkości szybciej.

0

@Shalom: Próbowałem z

@Transactional(isolation = Isolation.SERIALIZABLE) 

ale wtedy widzę, że i tak próbuje zrobić drugi raz insert do tabelki Order, i leci błąd PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions.
Details: Reason code: Canceled on identification as a pivot, during comit attempt.
Hint: The transaction might succed if retried.

I nie mam za bardzo pomysłu jak to ugryźć. Co do samej wydajności, dużo danych tutaj nie spływa, natomiast jak już spływają to w bardzo krótkich odstępach czasu, także to rozwiązanie może by przeszło.

0

Na bogato to stawiasz Rabitmq przed api obsługujesz kolejkę. Ale jak danych jest nie dużo to wystarczy tak jak zaproponował @Shalom

2

Nie znam się dobrze na JPA, ale trochę na SQL i z tej perspektywy dodaję komentarz. Czy jeśli pole someNumber dla encji *Order *jest unikalne, to czy nie może być taka kolumna kluczem głównym w tabeli? Wtedy zawsze będziesz mieć jeden rekord o tym identyfikatorze w tabeli. Wtedy kto pierwszy, ten lepszy - taki rekord zostanie dodany tylko raz nawet przy współbieżnym zapisie.

Jeśli chcesz aktualizować tę encję, to myślałeś może o wykorzystaniu optymistic locking i dodaniu pola z wersją?

Zakładając, że masz takie typowe zamówienie, które ma wiele pozycji, to zastanów się czy nie można tego inaczej też zamodelować. To znaczy odkładać sobie w innej tabelce informacji, że "dodano pozycję do zamówienia", "usunięto pozycję z zamówienia" i na bazie tych informacji obliczać aktualny stan zamówienia.

0

@Bronzebeard ale w takiej sytuacji musisz mieć jakiś retry ;) Bo jak masz izolacje repeatable reads to możesz mieć dwa wątki które zrobią orderRepository.findByNumber(dto.getSomeNumber) i oba będą myślały że rekordu jeszcze nie ma (bo nie ma!) i spróbują zrobić create i ten który wykona się później dostanie fail i trzeba by spróbować go powtórzyć. Tylko ze to problem, bo co jeśli zaraz po tych 2 pierwszych requestach przyszedł trzeci i on już zobaczył że nie jest nowy więc zrobił update... zanim twój retry tego sfailowanego się wykonał? :) Nagle możesz nadpisać dane przetwarzajac requesty w złej kolejności...

1
Bronzebeard napisał(a):

Nie znam się dobrze na JPA, ale trochę na SQL i z tej perspektywy dodaję komentarz. ...

Miałem pomysł wypowiedzieć się tu na gruncie SQL a nie obiektowym, ale nie szło to ładnie ułożyć. Nie miałem swobodnej głowy.

Ale dokładnie tak, masz rację: w ujęciu bazy relacyjnej jest to o wiele prostsze do wyrażenia, niż w modelu obiektowym, który nas kopie poniżej nerek.

Nie pierwszy raz mamy problem obiektowo-relacyjny

Ja bym szedł w jakieś repozytotium stanowo-obiektowe (tj udostępniające metody aktualizujTak(arg) aktualizujSiak(arg)), bym sobie zaimplementował np w JDBI
Zwłaszcza, że mój umysł nigdy nie był zniewolony REST-em (odczyt-zapis stanu), i ujęcia "czasownikowe" są w mojej głowie akceptowalne (choć mi autentycznie szkoda, że mniej obiektowe - choć może ktoś przemyśli to lepiej, i będzie obiektowe)

Sekwencja odczyt-zapisz stan jest "od zawsze" obszarem trudnym, od najgłupszego mikroprocesora, po super-hiper-korpo systemy.

2

Przyznam że nie bardzo rozumiem o co wam chodzi z tym JPA vs SQL. Przecież równie dobrze kolega mógłby tam robić gołe selecty, inserty i updaty, gdzie niby jest jakaś różnica? Chyba ze masz bazę która wspiera natywnie upsert

0
Shalom napisał(a):

Chyba ze masz bazę która wspiera natywnie upsert

Jednak większość sensownych baz (PostgreSQL, Oracle) rozwiązuje problem upsertu

1

@KamilAdam: no przez jakiejś skrypty da się zrobić upsert w Oraclu czy Postgresie, ale miałbym wątpliwości czy takie coś zastosować ;) Wymagałoby to pewnie jakiś procedur składowanych, a to raczej nie jest zbyt fajne dla programistów ;)

1
ProgScibi napisał(a):

@KamilAdam: no przez jakiejś skrypty da się zrobić upsert w Oraclu czy Postgresie, ale miałbym wątpliwości czy takie coś zastosować ;) Wymagałoby to pewnie jakiś procedur składowanych, a to raczej nie jest zbyt fajne dla programistów ;)

Niezgodzę się :). Oracle odkąd pamiętam ma magiczne polecenie MERGE które może zrobić INSERT lub UPDATE. Za to PostgreSQL od kilku lat ma INSERT … ON CONFLICT … DO UPDATE SET które precyzyjnie rozwiązuje problem Upsertu

0

No w sumię trochę racja, tzn chyba jednak da się zrobić to przez wywołanie natywnego zapytania w SQL bez stosowania procedury składowanej.
https://stackoverflow.com/questions/33910075/jpahibernate-and-postgres-sql-batch-upsert-using-nativequery

1 użytkowników online, w tym zalogowanych: 0, gości: 1