2011-12-12 75 views
3

是否有可能創建所有servlet都將使用的內存計數器?全局內存計數器,線程安全並每x增量刷新到mysql

此全局計數器將跟蹤Web應用程序的綜合瀏覽量,計數器將特定於當前登錄的用戶。即該集合將對每個用戶都有一個關鍵。

globalCounterMap[userId].incrementCounter += 1; 

在一定的時間間隔或計數的瀏覽量,我想保存當前計數到MySQL(插入新行),例如:

table_pageviews [id, userId, pageview_count, date] 

所以這個計數器隨後會後重置爲0沖洗。

所以,如果我有一個BaseServlet,所有的servlet將繼承,我將如何定義此字段? (最後,靜態?)

ConcurrentHashMap是否合適?也許我可以爲每個條目存儲AtomicLong的值。 http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/atomic/AtomicLong.html

我必須沖洗到MySQL過程中同步:

在沖洗,我可以通過設置爲0,並保存我「獲取」值,用原子長的getAndSet? (說我這樣做是每1K瀏覽量)

更新

所以,即使我有10個服務器,每個都有自己的內存計數器,事情仍會起作用,因爲他們都將最終刷新它們的計數數據庫,然後我會簡單地聚合行以獲得最終的計數。

+0

爲什麼沒有MemcacheD服務器,並將其每小時或分鐘刷新到DB? MemcachedD IO足夠快,並且它將始終保存用戶的總頁面瀏覽量。 – Nishant

回答

2

我會使用同步開始,因爲這是最簡單的方法。否則,您可能在收集數據和刷新結果之間使用頁面時遇到問題。你可以使用getAndSet(),但是如果你期望它是128而現在是130,你會怎麼做?

另一種選擇是不擔心它是完全線程安全的,幾個頁面更新丟失,不用擔心。

另一種方法是將總數寫入數據庫到目前爲止,只發送數據庫差異。這樣,就沒有必要重新設置數字(但是快照可能不是一次全部)

+0

+1對於「不完全是線程安全的」 – ewernli

2

這是可能的;但不可取。使用我的精神超級大國,我推斷你試圖實施一些統計數據收集工具,並且你希望每個時間間隔內每個用戶都有統計數據。

您可以使用Servlet過濾器和synchronized方法的做法,這將不時更新數據庫的時間 但你會碰到有問題的: - 在數臺服務器 應用集羣 - 管理數據庫連接和事務 (你會如果您不需要實時統計信息,則不開發此工具,否則您可以每24小時堅持一次日誌處理)

對於NoSQL數據庫(如redis)和某些密鑰的值的原子增量,這樣做會更好。只需使用「userid:startOfIntervalInMisllisecondsSince1970」作爲鍵,然後增加此值。 - 速度很快 - 原子 - 數據始終安全 - 無需共享任何內容,並跨負載平衡羣集或容器中多個線程之間進行同步。

+0

嗯,我正在向數據庫中插入一個新行,所以不管其他服務器是否執行相同的操作,因爲最終值將來自所有行的SUM,沒有? – codecompleting

+0

從我的數據模型我可以推斷,你將最終與具有相同的用戶ID和時間但具有相同ID的條目加載。您可以稍後運行聚合查詢,這可能證明您的數據大小不適合您的數據大小 –

3

Konstantin說,像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 
    } 


} 

的代碼相當複雜,和彼得Lawrey說,一個簡單的地圖保護,一個同步的關鍵字可能表現得不錯,並且更容易維護。

+1

用於刷新,我無法複製現有地圖,然後重置活動地圖,然後刷新副本?並確保在單個線程上處理事件。 – codecompleting

+0

如果通過重置你的意思是調用map.clear(),那麼不,這不會是線程安全的。您需要知道從映射中移除的AtomicIntegers,以將它們的值設置爲MIN_VALUE。如果通過重置,你的意思是在你複製之後有一些像counters = new ConcurrentHashMap()那麼是的,但是計數器必須是易變的,並且它實際上並不真的給你買東西 – sbridges

+0

如果你使用counters = new ConcurrentHashMap(),這可能不會是線程安全的 – sbridges

0

在這種情況下,計數器的準確性是不必要的。

我會去ConcurrentHashMap,但AtomicInteger可能沒有必要。假設客戶端的請求在同一臺服務器上交付,您可以使用輕鬆的線程安全來計算該客戶端的訪問頁數。您可能希望將計數排除在資源請求(例如樣式表等)上並僅計算內容頁面。所以雖然相同的客戶端的兩個併發請求可能會發生衝突,並且更新會丟失,但情況很少,放鬆的方案足夠好。也就是說,實施一個線程安全的方案與AtomicInteger可能不會影響性能,無論如何,鑑於鎖被收購了很短的時間(基準任何人?)。

問題是,計數器的地圖是全球,所以如果你有幾臺服務器不會擴展。處理這種

  • 一種選擇是有粘性會話(反正一個很好的做法)。這樣,客戶端的請求總是到達同一臺服務器。我們基本上和以前一樣。

  • 處理此問題的另一個選項是對計數器在客戶端上訪問的頁面數。您瀏覽器請求服務器不時保存該值(可能發送值和時間戳)。當用戶離開頁面/站點以確保將數據刷新到服務器時,您可以攔截javascript(請參閱onBeforeUnload)。