Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 01.10.18 19:58
Оценка:
Друзья, подскажите, возможно ли, и если да, то как сделать такую вещь:

Имеется веб-приложение, реализованное на сервлетах в apache tomcat. В этом приложении есть некая страница отображения и добавления записей в некую БД (используется СУБД PostgreSQL, но это не принципиально). При формировании этой страницы много данных считывается из БД, на странице есть форма добавления новой записи. Обработчик формы добавляет новую запись в БД и дальше опять отправляет пользователю ту же страницу, при этом считывает из БД почти те же самые данные (в большом количестве) плюс новая добавленная запись тоже считывается. Трафик к БД можно уменьшить на порядки, если выделить функцию чтения-записи данных из БД в отдельный "фоновый сервлет", который будет раз в 5-10 минут "сбрасывать" новые записи в БД чтобы постоянно не дергать БД. Подскажите, в каком направлении "копать", чтоб реализовать такое? Как организовать передачу данных между "фоновым" и front-end сервлетами? Буду благодарен за ссылки по теме.

Спасибо.
Re: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 01.10.18 21:03
Оценка:
Здравствуйте, teapot2, Вы писали:

T>Друзья, подскажите, возможно ли, и если да, то как сделать такую вещь:


T>Трафик к БД можно уменьшить на порядки, если выделить функцию чтения-записи данных из БД в отдельный "фоновый сервлет", который будет раз в 5-10 минут "сбрасывать" новые записи в БД чтобы постоянно не дергать БД.


Если нужно только запись сделать пакетной, то не сервлет нужен, а очередь + фоновый поток срабатывающий каждые 5 минут, который берёт всё из очереди и записывает в бд


    static final ScheduledExecutorService backgroundService = Executors.newSingleThreadScheduledExecutor();
    static final LinkedBlockingQueue queue = new LinkedBlockingQueue();
    void onInit() {
        backgroundService.schedule(MyCLass::writeToDb, 5, TimeUnit.MINUTES);
        
    }
    public static void enqueue(Data data) {
        queue.add(data);
    }

    static void writeToDb() {
        ArrayList<Data> pending = new ArrayList<>();
        queue.drainTo(pending);
        for (Data data: pending) {
            ....
        }
    }
Re[2]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 02.10.18 05:59
Оценка:
Здравствуйте, bzig, спасибо за ответ.

У проблемы есть еще вторая сторона. Можно провести такую аналогию:

Каждый раз, когда формируется страница с формой на добавление, из БД считывается 1000 (сильно утрирую) последних записей, которые отображаются на этой странице. Если пользователь через форму отправит новую запись, то после ее добавления в БД для повторного формирования этой же страницы необходимо считать 1000 последних записей, из них 999 уже были считаны прошлый раз и еще одна — только что добавленная. Как избежать повторного считывания 999 записей а взять их "из предыдущего запуска сервлета" и дополнить только одной добавленной записью? Возможность изменения БД кем-то еще помимо этого процесса мы здесь сознательно игнорируем, разумеется.

Спасибо.
Re[3]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: GarryIV  
Дата: 02.10.18 06:27
Оценка:
Здравствуйте, teapot2, Вы писали:

T>Каждый раз, когда формируется страница с формой на добавление, из БД считывается 1000 (сильно утрирую) последних записей, которые отображаются на этой странице. Если пользователь через форму отправит новую запись, то после ее добавления в БД для повторного формирования этой же страницы необходимо считать 1000 последних записей, из них 999 уже были считаны прошлый раз и еще одна — только что добавленная. Как избежать повторного считывания 999 записей а взять их "из предыдущего запуска сервлета" и дополнить только одной добавленной записью? Возможность изменения БД кем-то еще помимо этого процесса мы здесь сознательно игнорируем, разумеется.


Слова REST, XMLHttpRequest, Ajax чего-то говорят? Не надо перечитывать список если нет такой необходимости. С другой стороны ничего не мешает сделать кеширование тяжелых запросов для БД. 1000 записей можно и в памяти кешировать без проблем, благо кешей для Java вагон. Кешировать на запись я бы не стал вот так сразу, но тоже можно через какую-нибудь persistent queue. Кеширование записи в память черевато потерями данных.
WBR, Igor Evgrafov
Re[4]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 02.10.18 07:26
Оценка:
Здравствуйте, GarryIV, Вы писали:


GIV>Слова REST, XMLHttpRequest, Ajax чего-то говорят? Не надо перечитывать список если нет такой необходимости.

Слова знакомые, но есть ограничение — использовать только чистые сервлеты. Никакого JS кода в HTML-страницах.

GIV>С другой стороны ничего не мешает сделать кеширование тяжелых запросов для БД. 1000 записей можно и в памяти кешировать без проблем, благо кешей для Java вагон. Кешировать на запись я бы не стал вот так сразу, но тоже можно через какую-нибудь persistent queue. Кеширование записи в память черевато потерями данных.

Буду благодарен за ссылки и пинки в нужном направлении.
Re[3]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 02.10.18 13:48
Оценка:
Здравствуйте, teapot2, Вы писали:

T>Здравствуйте, bzig, спасибо за ответ.


Совет, кстати, подходит только для данных уровня лайк в фэйсбуке. Важные данные лучше сохранять синхронно. Если запись быстрая, то проблем быть не должго. А вычитывание можно делать асинхронным + кэш в памяти

    
    static final ScheduledExecutorService backgroundService = Executors.newSingleThreadScheduledExecutor();
     final LinkedList<Data> cache = new LinkedList<>();
    
    void onInit() {
        backgroundService.schedule(this::reloadFromDb, 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes

    }
    public  synchronized Collection<Data> save(Data data) {
        saveToDb(data);
        cache.add(data);
        while (cache.size() > 1000) {
            cache.removeFirst();
        }
        return new ArrayList<>(cache); // copy because cache is not thread-safe 
    }

    private synchronized void reloadFromDb() {
        ArrayList<Data> recent = loadFromDb();
        cache.clear();
        cache.addAll(recent);
    }
Отредактировано 02.10.2018 23:24 мамут ушёл, и я пойду . Предыдущая версия . Еще …
Отредактировано 02.10.2018 15:24 мамут ушёл, и я пойду . Предыдущая версия .
Re[4]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: · Великобритания  
Дата: 02.10.18 22:52
Оценка: +1
Здравствуйте, bzig, Вы писали:

b> public static synchronized Collection<Data> save(Data data) {


b> private synchronized void reloadFromDb() {

avalon/2.0.6
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[5]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 02.10.18 23:23
Оценка:
Ну в браузере и не такое напишешь
Re[4]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 03.10.18 13:27
Оценка:
Здравствуйте, bzig, спасибо за ответ... но "не выходит каменный цветок".

Ругается вот на это:
B> void onInit() {
B> backgroundService.schedule(this::reloadFromDb, 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes
Не нравится ему ни this::reloadFromDb, ни просто reloadFromDb, никакая другая форма.

Я так понимаю, что в качестве первого параметра здесь должен идти не метод, а объект класса, реализующего интерфейс Runnable.

Ок, пытаемся "обмануть" систему. Создаем внутренний класс TimerJob (внутренний — чтоб был доступ к членам и методам нашего класса):

    public class TimerJob implements Runnable
    {
        public TimerJob(){}

        public void run()
        {
            reloadFromDb();
        }
    }


И дальше делаем вот так:
...
    backgroundService.schedule(new TimerJob(), 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes
...


И, о чудо, все компилируется!

Но не работает. Падает с такой диагностикой:

java.lang.NoClassDefFoundError: MyServlet$TimerJob
at EmployeeList.init(MyServlet.java:53)
at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1144)
at org.apache.catalina.core.StandardWrapper.loadServlet(StandardWrapper.java:1091)
at org.apache.catalina.core.StandardWrapper.allocate(StandardWrapper.java:773)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:133)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)


Как я понимаю, он просто не может инстанцировать объект вложенного класса.

В общем, зашел в тупик. Есть идеи, как выбраться?
Re[5]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 03.10.18 14:02
Оценка:
Вдогонку: с этими проблемами разобрался:

T>Здравствуйте, bzig, спасибо за ответ... но "не выходит каменный цветок".


T>Ругается вот на это:

B>> void onInit() {
B>> backgroundService.schedule(this::reloadFromDb, 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes
T>Не нравится ему ни this::reloadFromDb, ни просто reloadFromDb, никакая другая форма.
Это потому что у меня JDK седьмой, а этот код требует восьмого.

T>И, о чудо, все компилируется!

T>Но не работает. Падает с такой диагностикой:
T>[q]
T> java.lang.NoClassDefFoundError: MyServlet$TimerJob
Это потому что я не копировал в каталог tomcat'а class-файл для вложенного класса. Я копирую скриптом, теперь подправил и файл копируется.

Всем спасибо, все заработало.
Отредактировано 03.10.2018 14:47 teapot2 . Предыдущая версия .
Re[6]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 03.10.18 14:53
Оценка:
T>Это потому что у меня JDK седьмой, а этот код требует восьмого.
T>Это потому что я не копировал в каталог tomcat'а class-файл для вложенного класса. Я копирую скриптом, теперь подправил и файл копируется.

Я тебе советую потратить время и сделать на SpringBoot, тогда ничего никуда копировать не придётся и даже сам Томкат не нужен будет. Или хотя бы настроить Мавен, чтобы он тебе делал war и деплоил в Томкат, чтобы руками не копировать.
Re[5]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 03.10.18 14:54
Оценка:
Здравствуйте, teapot2, Вы писали:

T>Здравствуйте, bzig, спасибо за ответ... но "не выходит каменный цветок".


T>Ругается вот на это:

B>> void onInit() {
B>> backgroundService.schedule(this::reloadFromDb, 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes
T>Не нравится ему ни this::reloadFromDb, ни просто reloadFromDb, никакая другая форма.

Я код писал в браузере и это больше псевдо-код чем реальный. Обрати внимание, статик у метода и поля cache быть не должно, как правильно заметил коллега dot
Re[6]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 03.10.18 14:56
Оценка:
T>>Ругается вот на это:
B>>> void onInit() {
B>>> backgroundService.schedule(this::reloadFromDb, 5, TimeUnit.MINUTES); // sync with possible db changes every 5 minutes
T>>Не нравится ему ни this::reloadFromDb, ни просто reloadFromDb, никакая другая форма.

B>Я код писал в браузере и это больше псевдо-код чем реальный. Обрати внимание, статик у метода и поля cache быть не должно, как правильно заметил коллега dot


Да и вообще, если ты унёс код в другой класс, то синхронизации (возможно) больше нет, а значит будут проблемы.
Отредактировано 03.10.2018 14:57 мамут ушёл, и я пойду . Предыдущая версия .
Re[7]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 03.10.18 15:19
Оценка:
Здравствуйте, bzig, Вы писали:

B>>Я код писал в браузере и это больше псевдо-код чем реальный. Обрати внимание, статик у метода и поля cache быть не должно, как правильно заметил коллега dot

Тем не менее это помогло сделать работающий пример, так что спасибо.


B>Да и вообще, если ты унёс код в другой класс, то синхронизации (возможно) больше нет, а значит будут проблемы.

Ну вложенный класс — чисто номинальный. Он вызывает методы моего исходного класса, которые объявлены как синхронизированные (как я понимаю, правильно это называется "критическая секция").

Остался такой вопрос: Есть структура, представляющая данные в памяти. С этой структурой могут работать обработчики запросов doGet и doPost, а также мой метод сохранения в базе. Их надо развести по времени. Если doGet/doPost меняют данные, saveToDB должен ждать окончания, и наоборот. Я правильно понимаю, что никакими декларациями synhcronized здесь не поможешь и надо семафорить вручную?
Отредактировано 03.10.2018 15:21 teapot2 . Предыдущая версия .
Re[8]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 03.10.18 20:13
Оценка:
T>Остался такой вопрос: Есть структура, представляющая данные в памяти. С этой структурой могут работать обработчики запросов doGet и doPost, а также мой метод сохранения в базе. Их надо развести по времени. Если doGet/doPost меняют данные, saveToDB должен ждать окончания, и наоборот. Я правильно понимаю, что никакими декларациями synhcronized здесь не поможешь и надо семафорить вручную?

синхронизироваться всё равно на чём.

public static final Object GLOBAL_LOCK = new Object();
и потом из любого места програмы синхронизируйся на нём
synchronized (GLOBAL_LOCK) { /* within global lock */}


Только так превратится твоё веб приложение практически в однопоточную тыкву.

По хорошему, надо смотреть что можно параллелить и что нельзя. Например, если чтения можно, то уже лучше использовать ReentrantReadWriteLock. А ещё можно данные на независимые сегменты делить и лочить только сегмент, тогда те запросы, что работают с другим сегментом, будут исполняться параллельно.
Re[6]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: · Великобритания  
Дата: 04.10.18 09:28
Оценка:
Здравствуйте, bzig, Вы писали:

B>Ну в браузере и не такое напишешь

Да вообще, советовать использовать синглтоны, глобальные переменные и локи — моветон. Тесты как писать будешь-то? Не учи плохому. В случае, если что-то должно жить дольше сервлетов — есть сессия (HttpSession) или приложение (ServletContext), вот туда и надо это класть.

А вообще, надо с задачей разобраться, прежде чем думать как прикрутить конкретное решение.
Начнём с того, непонятно, зачем вдруг потребовалось "постоянно не дергать БД". По идее, если там идёт просто чтение, то psql делает это довольно эффективно и без блокировок. Но это желательно делать из read only транзакции.
А если дёрганье БД действительно является проблемой, т.к. дёргается очень часто — то предлагать глобальный статический лок — вряд ли решит проблему, может только чуток улучшит ситуацию.
В этом случае скорее всего просто стоит прикрутить какой-нибудь готовый кеш, взять какой-нибудь guava LoadingCache, чем пилить свой с квадратными колёсами.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[9]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 04.10.18 10:23
Оценка:
Здравствуйте, bzig, Вы писали:

B>
public static final Object GLOBAL_LOCK = new Object();
и потом из любого места програмы синхронизируйся на нём

B>
synchronized (GLOBAL_LOCK) { /* within global lock */}

Примерно так я и представлял себе это.

B>Только так превратится твоё веб приложение практически в однопоточную тыкву.

Есть (большая и сложная) структура данных, данные из которой отображаются на html-страницах. Изменения в эту структуру вносятся методами класса сервлета doGet и doPost в ответ на действия пользователя. Параллельно (по таймеру в фоновом режиме) эта структура данных где-то сохраняется (в нашем случае в БД, но на самом деле не важно, в БД или в файле на диске). Понятно, что все эти операции не могут выполняться одновременно — иначе мы получим неконсистентные данные. Кроме того, чтение данных из структуры при генерации html-страницы совместимо с записью в файл но не совместимо с внесением изменения в данные. Какую блокировку здесь лучше всего использовать?
Re[7]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: teapot2  
Дата: 04.10.18 10:48
Оценка:
Здравствуйте, bzig, Вы писали:

B>Я тебе советую потратить время и сделать на SpringBoot, тогда ничего никуда копировать не придётся и даже сам Томкат не нужен будет. Или хотя бы настроить Мавен, чтобы он тебе делал war и деплоил в Томкат, чтобы руками не копировать.

Потом, наверное, так и сделаю. А сейчас надо быстро дать решение.

Еще один вопрос появился.

Дома (под виндой) я проверяю отдельные части решения на автономном томкате, который я поднимаю и гашу соответствующими батниками в каталоге ${Catalina.base}/bin. После того как я настроил запуск фонового процесса сохранения данных в БД после "гашения" томката его окошко перестало закрываться. Очевидно, виноват мой "фоновый" поток сохранения данных, о чем недвусмысленно пишется в лог Каталины:

04-Oct-2018 13:26:41.525 WARNING [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads The web application [MyWebApp] appears to have started a thread named [pool-1-thread-1] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1090)
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:807)
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
java.lang.Thread.run(Thread.java:745)


Запускаю фоновый поток так:
    public class TimerJob implements Runnable
    {
        public TimerJob(){}
        public void run()
        {
            saveDataToDb();
        }
    }
...
    private static final ScheduledExecutorService backgroundService = Executors.newSingleThreadScheduledExecutor();
...
    private static ScheduledFuture sf; // Sheduler handle
    sf = backgroundService.scheduleAtFixedRate(new TimerJob(), 1, 1, TimeUnit.MINUTES);


Останавливаю так (в методе destroy()):
    sf.cancel(false); // true нельзя, т.к. если поток будет завершен во время записи в БД, консистентность данных будет потеряна!!!


Но, вероятно, что-то не доделываю, раз поток управления остается "висеть". Как мне корректно "прибрать за собой" в этой ситуации?
Re[8]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 04.10.18 12:10
Оценка:
T>Останавливаю так (в методе destroy()):
T>
T>    sf.cancel(false); // true нельзя, т.к. если поток будет завершен во время записи в БД, консистентность данных будет потеряна!!!
T>


Это ты не поток остановил, а задачу снял. Для остановки пула надо backgroundService.shutdown или как-то там
Re[7]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
От: bzig  
Дата: 04.10.18 12:16
Оценка:
·>Да вообще, советовать использовать синглтоны, глобальные переменные и локи — моветон. Тесты как писать будешь-то? Не учи плохому. В случае, если что-то должно жить дольше сервлетов — есть сессия (HttpSession) или приложение (ServletContext), вот туда и надо это класть.

Я сервлеты плохо знаю, вот ты и посоветуй.

·>А если дёрганье БД действительно является проблемой, т.к. дёргается очень часто — то предлагать глобальный статический лок


Я предлагал глобальный, если производительность совсем не нужна.

·>В этом случае скорее всего просто стоит прикрутить какой-нибудь готовый кеш, взять какой-нибудь guava LoadingCache


Он только нужен, если ты хочешь что-нибудь больше чем кэш, типа eviction policy, слабые ссылки и прочее. Простейший кэш проще без него сделать.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.