Java – global memory counter, which is thread safe and flushed to MySQL every x increments
Can I create in memory counters that all servlets will use?
This global counter will track the web page views of the web application, and the counter will be specific to the currently logged in user That is, the collection will have a key for each user
globalCounterMap[userId].incrementCounter += 1;
In an interval or web browsing count, I want to save the current count to MySQL (insert a new line), for example:
table_pageviews [id,userId,pageview_count,date]
Therefore, this counter will be reset to 0 after refresh
So, if I have a baseservlet that all servlets will inherit, how do I define this field? (finally, static?)
Is concurrenthashmap appropriate? Maybe I can store an atomiclong value for each entry
During refresh, I can use getandset of atomic long by setting it to 0 and save the value I get: http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/atomic/AtomicLong.html
Must I synchronize during refresh to MySQL process? (suppose I do this for every 1K page views)
to update
So even if I have 10 servers and each server has its own memory counter, things still work because they will eventually refresh their counts to the database, and then I will simply aggregate rows to get the final count
Solution
Constantine said something like redis might be a better solution Cassandra counter is also a good way to do such things
If you want to do this in Java, here are some code that can safely increase the count without blocking,
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 } }
The code is quite complex. As Peter Lawley said, a simple mapping protected with the synchronized keyword may perform well enough and be easier to maintain