System do wydawania kuponów gdzie miałem się skupić na funkcjonalności do zapewnienie, że jeden kupon może być użyty tylko określoną liczbę razy. Z takich wymagań konkretnych to wysokie availability 99,999, response poniżej 100ms, założenie że będzie 2000tps
Jakbyście do tego podeszli? ChatGPT sugeruje
@Transactional @lock(LockModeType.PESSIMISTIC_WRITE) @query("SELECT k FROM Kupon k WHERE k.kod = :kod") Optional<Kupon> findByKodForUpdate(String kod); i
@Transactional public boolean wykorzystajKupon(String kod) { return kuponRepository.findByKodForUpdate(kod).map(kupon -> { if (kupon.getWykorzystania() < kupon.getLimitUzyc()) { kupon.setWykorzystania(kupon.getWykorzystania() + 1); kuponRepository.save(kupon); return true; } else { return false; } }).orElse(false); } już pomijając polskie nazwy i save na już persisted encji Czy ma to sens, czy jest to wystarczające? Zakładając, że tak - to czy byłoby to wydajne i skalowane? W NoSQLowej bazie byłby pewnie łatwiej o wydajność, ale czy da się wtedy zrobić takiego locka?
Dzięki wszystkim za dyskusję! To ja to widzę tak: 1. Relacyjność raczej musi być - wątpię, aby w docelowym systemie nie chcielibyśmy powiązać kuponu z użytkownikiem i zamówieniem 2. Jednak aby zachować szybki response przechowywałbym użyte kupony w jakiejś NoSQL bazie - pewnie jakiś cache (Redis). 3. Gdy dostaniemy odpowiedź, że kupon jest/był dostępny to łączyłbym się już z transakcyjną bazą danych i w update tak jak sugerował @VlIN inkrementowałbym kupon
Jednak aby zachować szybki response przechowywałbym użyte kupony w jakiejś NoSQL bazie - pewnie jakiś cache (Redis).
@Patres: No ale tutaj też z kolei miałbyś kolejny narzut na synchronizację bazy z cache. Cache nie zawsze jest spójny z tym co jest na bazie. I to też nie byłoby takie trywialne jak to ugryźć.
@exori_vis: Niekoniecznie, w cache chcę trzymać informację czy kupon może być jeszcze użyty, więc jak np. mam 100 kuponów i 200 requestów to dopiero po setnym kuponie dodaje do cache (tylko raz, a jakby się stało, że już tam jest ten kupon to żeby nie dodało drugi raz tego kuponu do tej bazy).
100 kuponów | 200 requestów: 1..99 - brak reakcji z cache -> obsługą przez relacyjną bazę danych
@Patres jeśli chcesz żeby kupon mógł być wycofany gdy anulujesz zamówienie to w ramach jednej transakcji: - weź kupon z licznika kuponów przez ten UPDATE - jak się udało to dodaj do osobnej tabeli COUPON_USES event że kupon został wykorzystany w danym czasie i ew. jakiś stan wykorzystania kuponu, Reserved, used, cancelled - Powiąż jakoś ten event z rekordem zamówienia - commit transakcji
1. Relacyjność raczej musi być - wątpię, aby w docelowym systemie nie chcielibyśmy powiązać kuponu z użytkownikiem i zamówieniem
@Patres: a co niby z tym ma wspólnego relacyjność? Bazy relacyjne nie nazywają się relacyjne dlatego że między tabelami są związki (klucz obcy, klucz główny). W bazach NoSQL też takie związki między użytkownikiem a zamówieniem zamodelujesz.
2. Zdajesz sobie sprawę z tego że cały system bankowy / płatniczy zbudowano bez ACID?
@exori_vis:a co do poziomu izolacji transakcji to pytanie jest bez sensu bo poziomy izolacji dotyczą tylko baz relacyjnych. W ogólności poziomów spójności danych istnieje o wiele więcej niż to co potrafią bazy relacyjne.
Mirki, czytam sobie różne pytania z system design i natrafiłem na coś takiego:
Jakbyście do tego podeszli? ChatGPT sugeruje
@Transactional
@lock(LockModeType.PESSIMISTIC_WRITE)
@query("SELECT k FROM Kupon k WHERE k.kod = :kod")
Optional<Kupon> findByKodForUpdate(String kod);
i
@Transactional
public boolean wykorzystajKupon(String kod) {
return kuponRepository.findByKodForUpdate(kod).map(kupon -> {
if (kupon.getWykorzystania() < kupon.getLimitUzyc()) {
kupon.setWykorzystania(kupon.getWykorzystania() + 1);
kuponRepository.save(kupon);
return true;
} else {
return false;
}
}).orElse(false);
}
już pomijając polskie nazwy i save na już persisted encji
Czy ma to sens, czy jest to wystarczające?
Zakładając, że tak - to czy byłoby to wydajne i skalowane? W NoSQLowej bazie byłby pewnie łatwiej o wydajność, ale czy da się wtedy zrobić takiego locka?
1. Relacyjność raczej musi być - wątpię, aby w docelowym systemie nie chcielibyśmy powiązać kuponu z użytkownikiem i zamówieniem
2. Jednak aby zachować szybki response przechowywałbym użyte kupony w jakiejś NoSQL bazie - pewnie jakiś cache (Redis).
3. Gdy dostaniemy odpowiedź, że kupon jest/był dostępny to łączyłbym się już z transakcyjną bazą danych i w update tak jak sugerował @VlIN inkrementowałbym kupon
@Patres: No ale tutaj też z kolei miałbyś kolejny narzut na synchronizację bazy z cache. Cache nie zawsze jest spójny z tym co jest na bazie. I to też nie byłoby takie trywialne jak to ugryźć.
100 kuponów | 200 requestów:
1..99 - brak reakcji z cache -> obsługą przez relacyjną bazę danych
- weź kupon z licznika kuponów przez ten UPDATE
- jak się udało to dodaj do osobnej tabeli COUPON_USES event że kupon został wykorzystany w danym czasie i ew. jakiś stan wykorzystania kuponu, Reserved, used, cancelled
- Powiąż jakoś ten event z rekordem zamówienia
- commit transakcji
@VlIN: Operacja update nie zwraca nic,przynajmniej tak jest w JPA w Javie, ewentualnie Integer
@Patres: a co niby z tym ma wspólnego relacyjność? Bazy relacyjne nie nazywają się relacyjne dlatego że między tabelami są związki (klucz obcy, klucz główny). W bazach NoSQL też takie związki między użytkownikiem a zamówieniem zamodelujesz.
2. Zdajesz sobie sprawę z tego że cały system bankowy / płatniczy zbudowano bez ACID?