Глобальный счетчик в памяти, который является потокобезопасным и сбрасывается в mysql каждые x приращений - PullRequest
3 голосов
/ 12 декабря 2011

Можно ли создать счетчик в памяти, который будут использовать все сервлеты?

Этот глобальный счетчик будет отслеживать просмотры страниц для веб-приложения, и этот счетчик будет зависеть от текущего пользователя, вошедшего в систему. то есть в коллекции будет ключ для каждого пользователя.

globalCounterMap[userId].incrementCounter += 1;

Через определенный интервал или количество просмотров страниц я хочу сохранить текущий счетчик в mysql (вставить новую строку), например ::

table_pageviews [id, userId, pageview_count, date]

Таким образом, этот счетчик будет сброшен на 0 после сброса.

Итак, если у меня есть BaseServlet, от которого будут наследоваться все сервлеты, как бы я определил это поле? (окончательно, статично?)

Подходит ли ConcurrentHashMap? Может быть, я могу сохранить AtomicLong для значения для каждой записи.

Во время сброса я могу использовать getAndSet атомарного длинного, установив в 0 и сохранив полученное значение: http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/atomic/AtomicLong.html

Нужно ли синхронизировать процесс flush to mysql? (Скажем, я делаю это каждые 1К просмотров)

Обновление

Так что, даже если у меня есть 10 серверов, каждый со своими счетчиками в памяти, все будет работать, поскольку все они в конечном итоге сбросят свои значения в БД, а затем я просто объединю строки, чтобы получить окончательные значения.

Ответы [ 4 ]

3 голосов
/ 20 декабря 2011

Как сказал Константин, что-то вроде redis может быть лучшим решением.Счетчики Кассандры - также очень хороший способ сделать подобные вещи.

Если вы хотите сделать это с Java, вот код, который увеличивает счет безопасно и без блокировки,

class Counter {

    private final ConcurrentHashMap<String, AtomicInteger> counts = new ConcurrentHashMap<String, AtomicInteger>();

    //increment the count for the user
    public void increment(String user) {
        while(true) {
            AtomicInteger current = counts.get(user);
            if(current == null) {
                //new user, initialize the count
                counts.putIfAbsent(user, new AtomicInteger());
                continue;
            }

            int value = current.incrementAndGet();
            if(value > 0) {
                //we have incremented the counter
                break;
            } else {
                //someone is flushing this key, remove it
                //so we can increment on our next iteration
                counts.replace(user, current, new AtomicInteger());
            }

        }
    }

    //call this periodically to flush keys to the database
    //this will empty the counts map so that users who
    //are not active do not take up space
    public void flush() {
        Map<String, Integer> toFlush = new HashMap<String, Integer>();

        for(Map.Entry<String, AtomicInteger> entry : counts.entrySet()) {
            String user = entry.getKey();
            AtomicInteger currentCount = entry.getValue();
            //stop incrementing this count
            counts.remove(user, currentCount);
            //if someone is trying to increment this AtomicInteger after
            //we remove it, they will see a -ve value from incrementAndGet, and 
            //will know their increment did not succeed
            Integer count = currentCount.getAndSet(Integer.MIN_VALUE);
            toFlush.put(user, count);
        }

        for(Map.Entry<String, Integer> clearedEntry : toFlush.entrySet()) {
            writeToDb(clearedEntry.getKey(), clearedEntry.getValue());
        }

    }

    public void writeToDb(String user, int count) {
        //do something with the count here
    }


}

Код довольно сложный, и, как сказал Питер Лори, простая карта, защищенная синхронизированным ключевым словом, вероятно, будет работать достаточно хорошо и будет намного проще в обслуживании.

2 голосов
/ 12 декабря 2011

Это возможно; но не рекомендуется. Используя мои интеллектуальные сверхспособности, я делаю вывод, что вы пытаетесь внедрить какой-то инструмент сбора статистики, и вам нравится, когда статистика накапливается на пользователя за каждый интервал времени.

Вы можете использовать подход с фильтром сервлетов и синхронизированным методом, который будет время от времени обновлять базу данных. Но вы столкнетесь с проблемами: - кластеризация приложений на нескольких серверах - управление подключениями к базам данных и транзакциями (вы не будете разрабатывать этот инструмент, если вам не нужна статистика в реальном времени, в противном случае вы можете просто продолжать обработку журналов каждые 24 часа)

Это лучше сделать с базой данных NoSQL, такой как redis, и атомарными приращениями значения некоторого ключа. Просто используйте «userid: startOfIntervalInMisllisecondsSince1970» в качестве ключа и увеличьте это значение. - это быстро - атомный - данные всегда в безопасности - нет необходимости делиться чем-либо и синхронизироваться между кластером с балансировкой нагрузки или между несколькими потоками в вашем контейнере.

2 голосов
/ 12 декабря 2011

Для начала я бы использовал синхронизированный, так как это самый простой подход. В противном случае у вас могут возникнуть проблемы с использованием страниц между сбором данных и сбросом результата. Вы можете использовать getAndSet (), но что вы будете делать, если ожидаете, что он будет 128, а теперь 130?

Другой вариант - не беспокоиться о том, что он полностью поточнобезопасен, а несколько обновлений страниц потеряны, не беспокойтесь.

Другой вариант - сохранить итоговое значение, записанное в базу данных, и отправить только различия в базу данных. Таким образом, нет необходимости сбрасывать числа (однако снимок может быть не все сразу)

0 голосов
/ 26 декабря 2011

Быть точным в отношении счетчика в этом случае не обязательно.

Я бы выбрал ConcurrentHashMap, но AtomicInteger может не понадобиться. Предполагая, что запросы клиента доставляются на тот же сервер, вы можете посчитать количество посещенных страниц для этого клиента с непринужденной безопасностью потоков. Возможно, вы захотите исключить счетчик для запроса ресурса (например, таблицы стилей и т. Д.) И считать только страницы содержимого. Таким образом, хотя два одновременных запроса одного и того же клиента могут конфликтовать, и обновление теряется, это происходит редко, и упрощенная схема достаточно хороша. Тем не менее, реализация поточно-ориентированной схемы с AtomicInteger, вероятно, не повлияет на производительность в любом случае, учитывая, что блокировка получена в течение очень короткого периода времени (кто-нибудь тестирует?).

Проблема в том, что карта счетчика имеет значение global , поэтому она не масштабируется, если у вас несколько серверов.

  • Один из способов справиться с этим - иметь липкие сессии (в любом случае, хорошая практика). Таким образом, запросы клиента всегда доходят до одного и того же сервера. Мы в основном в той же ситуации, что и ранее.

  • Другой вариант для обработки этого - счетчик количества страниц, посещенных на стороне клиента . Ваш браузер запрашивает у сервера время от времени сохранять значение (возможно, отправляя значение и отметку времени). Вы можете перехватить javascript, когда пользователь покидает страницу / сайт, чтобы убедиться, что вы сбрасываете данные на сервер (см. onBeforeUnload).

...