Niestosowna obsługa pamięci w aplikacji bibliotecznej o dużej skali

0

Podejście to wydaje się być niewłaściwe z następujących powodów:

List<AppUserEmailBooks> appUserEmailBooksList = new ArrayList<>();

Użycie pamięci: Przechowywanie wszystkiego w pamięci stanowi problem, zwłaszcza biorąc pod uwagę, że ta aplikacja jest przeznaczona dla biblioteki publicznej, gdzie może być dziesiątki lub nawet setki milionów użytkowników.

Wydajność wyszukiwania: Wyszukiwanie użytkownika za każdym razem, gdy chcę dodać mu książki do listy, stanie się coraz bardziej czasochłonne w miarę wzrostu liczby obsługiwanych przez aplikację użytkowników. W praktyce ten proces może trwać kilka dni, nawet na zwykłym komputerze, a być może nawet dłużej.

Już próbowałem korzystać z Map, ale jest to jeszcze bardziej obciążające dla pamięci, zwłaszcza przy dużych ilościach danych.

Szukam porady, jak poprawić tę sytuację. Będę wdzięczny za wszelkie sugestie dotyczące alternatywnych podejść lub najlepszych praktyk dotyczących obsługi dużych zbiorów danych w sposób efektywny pod względem pamięci i wydajności. Myslicie moze ze Spring Batch bedzie dobrym podejsciem tutaj, i nie bedzie zjadac zasobow pamieci?

Program ma na celu zaspokojenie potrzeb biblioteki, która chce dodawać książki, rejestrować użytkowników i subskrypcje. Jeśli użytkownik jest zapisany na kategorię, autora lub obie, powinno mu być wysyłane codziennie jedno powiadomienie e-mailowe o nowej książce. Nie powinny być wysyłane duplikaty, jeśli użytkownik jest zapisany zarówno na kategorię, jak i autora.

@Component
@RequiredArgsConstructor
public class EmailSchedule {

    private final EmailService emailService;

    private final BookRepository bookRepository;

    private final SubscriptionRepository subscriptionRepository;

    @Scheduled(cron = "${scheduled.email.notification.cron}")
    public void sendScheduledEmailNotification() {
        LocalDate todayDateTime = LocalDate.now();
        final int pageSize = 10000;
        Page<Book> pageResult;
        Pageable pageable = PageRequest.of(0, pageSize);

        List<AppUserEmailBooks> appUserEmailBooksList = new ArrayList<>();

        do {
            pageResult = bookRepository.findAllByAddedDateToday(todayDateTime, pageable);
            List<Book> newBooks = pageResult.getContent();

            processSubscriptionsInBatches(newBooks, appUserEmailBooksList);

            pageable = pageResult.nextPageable();
        } while (pageResult.hasNext());

        for (AppUserEmailBooks appUserEmailBooks : appUserEmailBooksList) {
            String userEmail = appUserEmailBooks.getUserEmail();
            List<Book> matchedBooks = appUserEmailBooks.getMatchedBooks();
            emailService.sendNotificationIfNewBooks(userEmail, new ArrayList<>(matchedBooks));
        }
    }

    public void processSubscriptionsInBatches(List<Book> newBooks, List<AppUserEmailBooks> appUserEmailBooksList) {
        int batchSize = 100;
        int page = 0;

        Pageable subscriptionPageable = PageRequest.of(page, batchSize);
        Page<Subscription> subscriptionPage;

        do {
            subscriptionPage = subscriptionRepository.findAll(subscriptionPageable);
            List<Subscription> subscriptions = subscriptionPage.getContent();

            for (Subscription subscription : subscriptions) {
                Set<Book> matchedBooks = matchBooksToSubscription(subscription, newBooks);

                if (!matchedBooks.isEmpty()) {
                    String userEmail = subscription.getAppUser().getEmail();
                    updateUserEmailBooksList(userEmail, matchedBooks, appUserEmailBooksList);
                }
            }

            subscriptionPageable = subscriptionPage.nextPageable();
            page++;
        } while (subscriptionPage.hasNext());
    }

    public void updateUserEmailBooksList(String userEmail, Set<Book> matchedBooks, List<AppUserEmailBooks> appUserEmailBooksList) {
        for (AppUserEmailBooks appUserEmailBooks : appUserEmailBooksList) {
            if (appUserEmailBooks.getUserEmail().equals(userEmail)) {
                appUserEmailBooks.getMatchedBooks().addAll(matchedBooks);
                return;
            }
        }
        appUserEmailBooksList.add(new AppUserEmailBooks(userEmail, new ArrayList<>(matchedBooks)));
    }

    public Set<Book> matchBooksToSubscription(Subscription subscription, List<Book> newBooks) {
        Set<Book> matchedBooks = new HashSet<>();
        for (Book book : newBooks) {
            if (book.getAuthor().equals(subscription.getBookAuthor()) ||
                    book.getCategory().equals(subscription.getBookCategory())) {
                matchedBooks.add(book);
            }
        }
        return matchedBooks;
    }
}

@Service
public class EmailService {

    private final JavaMailSender mailSender;

    @Value("${library.base-url}")
    private String baseUrl;

    public EmailService(JavaMailSender mailSender, @Value("${library.base-url}") String baseUrl) {
        this.mailSender = mailSender;
        this.baseUrl = baseUrl;
    }

    public void sendConfirmationEmail(String to, String subject, String token) {

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(subject);

        String link = baseUrl + "?token=" + token;

        StringBuilder messageContent = new StringBuilder("<p>Thank you for registering, please confirm your email address!</p>\n" +
                "<p>Click the button to confirm:</p>" + link);

        message.setText(String.valueOf(messageContent));
        mailSender.send(message);

    }


    public void sendSimpleMessage(String to, String subject, String text) {

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to.trim());
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }

    public void sendNotificationIfNewBooks(String to, List<Book> books) {

        String subject = "New books added to the book store!";

        StringBuilder textBuilder = new StringBuilder("New books added to the store, take a look at the new positions below! :\n");
        for (Book book : books) {
            textBuilder.append("Title: ").append(book.getTitle())
                    .append(" Author: ").append(book.getAuthor())
                    .append(" Category: ").append(book.getCategory())
                    .append("\n\n");

        }

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(subject);
        message.setText(textBuilder.toString());

        mailSender.send(message);

    }
}
public interface BookRepository extends JpaRepository<Book, Long> {

    @Query("SELECT b FROM Book b WHERE b.addedDate = CURRENT_DATE")
    Page<Book> findAllByAddedDateToday(LocalDate addedDate, Pageable pageable);
0

Napiszę tak - wątpię, żeby problem z czasem przetwarzania był związany z pamięcią (nawet jakbyś miał milion rekordów dziennie i każdy rekord ważyłby kilobajt to razem wyjdzie mniej niż giga), ale bardzo łatwo go rozwiązać - zamiast:

do {
    pageResult = bookRepository.findAllByAddedDateToday(todayDateTime, pageable);
    List<Book> newBooks = pageResult.getContent();

    processSubscriptionsInBatches(newBooks, appUserEmailBooksList);

    pageable = pageResult.nextPageable();
} while (pageResult.hasNext());

for (AppUserEmailBooks appUserEmailBooks : appUserEmailBooksList) {
    String userEmail = appUserEmailBooks.getUserEmail();
    List<Book> matchedBooks = appUserEmailBooks.getMatchedBooks();
    emailService.sendNotificationIfNewBooks(userEmail, new ArrayList<>(matchedBooks));
}

Napisz:

do {
    pageResult = bookRepository.findAllByAddedDateToday(todayDateTime, pageable);
    List<Book> newBooks = pageResult.getContent();

    processSubscriptionsInBatches(newBooks, appUserEmailBooksList);

    pageable = pageResult.nextPageable();

    for (AppUserEmailBooks appUserEmailBooks : appUserEmailBooksList) {
      String userEmail = appUserEmailBooks.getUserEmail();
      List<Book> matchedBooks = appUserEmailBooks.getMatchedBooks();
      emailService.sendNotificationIfNewBooks(userEmail, new ArrayList<>(matchedBooks));
    }
    appUserEmailBooksList.clear();
} while (pageResult.hasNext());

i w rezultacie nie będziesz miał problemów z pamięcią, bo maksymalny rozmiar appUserEmailBooksList będzie ograniczony.

Jeśli już gdzieś miałbym w twoim przypadku szukać wąskich gardeł to najprawdopodobniej problemem będą zewnętrzne zależności, czyli konkretnie - EmailService. Zakładając, że round-trip (czyli wysłanie informacji i odbiór odpowiedzi) będzie trwał jakieś 200 ms i będziesz miał ten milion rekordów to dostaniesz dodatkowe 200 tys. sekund okładu, czyli 55 godzin.

Spring Batch MOŻE pomóc, tak samo jak dowolny silnik pozwalający na procesowanie batchy dzięki opcji przetwarzania równoległego pewnych kroków. Można też to zrównoleglić w kodzie, który masz - wszystko zależy od infrastruktury (ile równoległych zapytań twój serwer email jest w stanie uciągnąć) oraz od liczby rekordów z jaką przyjdzie ci się mierzyć.

Zagadnienie "jak przetwarzać duże ilości danych" to sprawa skomplikowana, są do tego silniki typu Apache Flink - można też pisać własne skrypty czy użyć np. Airflow.

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