Друзья, подскажите, возможно ли, и если да, то как сделать такую вещь:
Имеется веб-приложение, реализованное на сервлетах в apache tomcat. В этом приложении есть некая страница отображения и добавления записей в некую БД (используется СУБД PostgreSQL, но это не принципиально). При формировании этой страницы много данных считывается из БД, на странице есть форма добавления новой записи. Обработчик формы добавляет новую запись в БД и дальше опять отправляет пользователю ту же страницу, при этом считывает из БД почти те же самые данные (в большом количестве) плюс новая добавленная запись тоже считывается. Трафик к БД можно уменьшить на порядки, если выделить функцию чтения-записи данных из БД в отдельный "фоновый сервлет", который будет раз в 5-10 минут "сбрасывать" новые записи в БД чтобы постоянно не дергать БД. Подскажите, в каком направлении "копать", чтоб реализовать такое? Как организовать передачу данных между "фоновым" и front-end сервлетами? Буду благодарен за ссылки по теме.
Спасибо.
Re: Сайт на сервлетах apache tomcat - "фоновый сервлет"
Здравствуйте, 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 - "фоновый сервлет"
У проблемы есть еще вторая сторона. Можно провести такую аналогию:
Каждый раз, когда формируется страница с формой на добавление, из БД считывается 1000 (сильно утрирую) последних записей, которые отображаются на этой странице. Если пользователь через форму отправит новую запись, то после ее добавления в БД для повторного формирования этой же страницы необходимо считать 1000 последних записей, из них 999 уже были считаны прошлый раз и еще одна — только что добавленная. Как избежать повторного считывания 999 записей а взять их "из предыдущего запуска сервлета" и дополнить только одной добавленной записью? Возможность изменения БД кем-то еще помимо этого процесса мы здесь сознательно игнорируем, разумеется.
Спасибо.
Re[3]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
Здравствуйте, teapot2, Вы писали:
T>Каждый раз, когда формируется страница с формой на добавление, из БД считывается 1000 (сильно утрирую) последних записей, которые отображаются на этой странице. Если пользователь через форму отправит новую запись, то после ее добавления в БД для повторного формирования этой же страницы необходимо считать 1000 последних записей, из них 999 уже были считаны прошлый раз и еще одна — только что добавленная. Как избежать повторного считывания 999 записей а взять их "из предыдущего запуска сервлета" и дополнить только одной добавленной записью? Возможность изменения БД кем-то еще помимо этого процесса мы здесь сознательно игнорируем, разумеется.
Слова REST, XMLHttpRequest, Ajax чего-то говорят? Не надо перечитывать список если нет такой необходимости. С другой стороны ничего не мешает сделать кеширование тяжелых запросов для БД. 1000 записей можно и в памяти кешировать без проблем, благо кешей для Java вагон. Кешировать на запись я бы не стал вот так сразу, но тоже можно через какую-нибудь persistent queue. Кеширование записи в память черевато потерями данных.
WBR, Igor Evgrafov
Re[4]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
GIV>Слова REST, XMLHttpRequest, Ajax чего-то говорят? Не надо перечитывать список если нет такой необходимости.
Слова знакомые, но есть ограничение — использовать только чистые сервлеты. Никакого JS кода в HTML-страницах.
GIV>С другой стороны ничего не мешает сделать кеширование тяжелых запросов для БД. 1000 записей можно и в памяти кешировать без проблем, благо кешей для Java вагон. Кешировать на запись я бы не стал вот так сразу, но тоже можно через какую-нибудь persistent queue. Кеширование записи в память черевато потерями данных.
Буду благодарен за ссылки и пинки в нужном направлении.
Re[3]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
Здравствуйте, 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);
}
Здравствуйте, 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 - "фоновый сервлет"
Вдогонку: с этими проблемами разобрался:
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-файл для вложенного класса. Я копирую скриптом, теперь подправил и файл копируется.
T>Это потому что у меня JDK седьмой, а этот код требует восьмого. T>Это потому что я не копировал в каталог tomcat'а class-файл для вложенного класса. Я копирую скриптом, теперь подправил и файл копируется.
Я тебе советую потратить время и сделать на SpringBoot, тогда ничего никуда копировать не придётся и даже сам Томкат не нужен будет. Или хотя бы настроить Мавен, чтобы он тебе делал war и деплоил в Томкат, чтобы руками не копировать.
Re[5]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
Здравствуйте, 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 - "фоновый сервлет"
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
Да и вообще, если ты унёс код в другой класс, то синхронизации (возможно) больше нет, а значит будут проблемы.
Здравствуйте, bzig, Вы писали:
B>>Я код писал в браузере и это больше псевдо-код чем реальный. Обрати внимание, статик у метода и поля cache быть не должно, как правильно заметил коллега dot
Тем не менее это помогло сделать работающий пример, так что спасибо.
B>Да и вообще, если ты унёс код в другой класс, то синхронизации (возможно) больше нет, а значит будут проблемы.
Ну вложенный класс — чисто номинальный. Он вызывает методы моего исходного класса, которые объявлены как синхронизированные (как я понимаю, правильно это называется "критическая секция").
Остался такой вопрос: Есть структура, представляющая данные в памяти. С этой структурой могут работать обработчики запросов doGet и doPost, а также мой метод сохранения в базе. Их надо развести по времени. Если doGet/doPost меняют данные, saveToDB должен ждать окончания, и наоборот. Я правильно понимаю, что никакими декларациями synhcronized здесь не поможешь и надо семафорить вручную?
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 - "фоновый сервлет"
Здравствуйте, bzig, Вы писали:
B>Ну в браузере и не такое напишешь
Да вообще, советовать использовать синглтоны, глобальные переменные и локи — моветон. Тесты как писать будешь-то? Не учи плохому. В случае, если что-то должно жить дольше сервлетов — есть сессия (HttpSession) или приложение (ServletContext), вот туда и надо это класть.
А вообще, надо с задачей разобраться, прежде чем думать как прикрутить конкретное решение.
Начнём с того, непонятно, зачем вдруг потребовалось "постоянно не дергать БД". По идее, если там идёт просто чтение, то psql делает это довольно эффективно и без блокировок. Но это желательно делать из read only транзакции.
А если дёрганье БД действительно является проблемой, т.к. дёргается очень часто — то предлагать глобальный статический лок — вряд ли решит проблему, может только чуток улучшит ситуацию.
В этом случае скорее всего просто стоит прикрутить какой-нибудь готовый кеш, взять какой-нибудь guava LoadingCache, чем пилить свой с квадратными колёсами.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[9]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
public static final Object GLOBAL_LOCK = new Object();
и потом из любого места програмы синхронизируйся на нём B>
synchronized (GLOBAL_LOCK) { /* within global lock */}
Примерно так я и представлял себе это.
B>Только так превратится твоё веб приложение практически в однопоточную тыкву.
Есть (большая и сложная) структура данных, данные из которой отображаются на html-страницах. Изменения в эту структуру вносятся методами класса сервлета doGet и doPost в ответ на действия пользователя. Параллельно (по таймеру в фоновом режиме) эта структура данных где-то сохраняется (в нашем случае в БД, но на самом деле не важно, в БД или в файле на диске). Понятно, что все эти операции не могут выполняться одновременно — иначе мы получим неконсистентные данные. Кроме того, чтение данных из структуры при генерации html-страницы совместимо с записью в файл но не совместимо с внесением изменения в данные. Какую блокировку здесь лучше всего использовать?
Re[7]: Сайт на сервлетах apache tomcat - "фоновый сервлет"
Здравствуйте, 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 - "фоновый сервлет"
·>Да вообще, советовать использовать синглтоны, глобальные переменные и локи — моветон. Тесты как писать будешь-то? Не учи плохому. В случае, если что-то должно жить дольше сервлетов — есть сессия (HttpSession) или приложение (ServletContext), вот туда и надо это класть.
Я сервлеты плохо знаю, вот ты и посоветуй.
·>А если дёрганье БД действительно является проблемой, т.к. дёргается очень часто — то предлагать глобальный статический лок
Я предлагал глобальный, если производительность совсем не нужна.
·>В этом случае скорее всего просто стоит прикрутить какой-нибудь готовый кеш, взять какой-нибудь guava LoadingCache
Он только нужен, если ты хочешь что-нибудь больше чем кэш, типа eviction policy, слабые ссылки и прочее. Простейший кэш проще без него сделать.