我正在计算页面上查看用户的数量。基本上,当用户查看URL localhost:8080/itemDetail.do?itemId=1
时,页面将显示有多少用户同时在该页面上查看。
解决方案方法
当用户正在查看某个特定页面时,该页面将继续每隔1秒向服务器发送AJAX请求,然后服务器将使用Map<String, Map<String, Integer>>
(均使用ConcurrentHashMap
进行初始化)来包含itemId
(以了解查看了哪个页面)以及IP
和timeout
计数(在内部Map
中)。每次收到请求时,计数将增加1。在请求过程中会触发一个线程,以减少每2秒的超时计数。如果最终,超时计数等于0,应用程序将认为用户已停止查看页面,然后线程将删除该条目,因此用户数量将减少1。这种方法的一个小问题是,由于超时数的增加速度快于减少速度,如果用户打开页面足够长时间并关闭页面,那么应用程序将需要一段时间才能知道该用户已经离开了页面,因为此时的超时数是相当大的
实施
// Controller
@RequestMapping(value = AppConstants.ACTION_NUMBER_VIEWING, method = GET)
@ResponseBody
public int userItemViewing(HttpServletRequest request, @RequestParam(value = "itemId") String itemId) {
try {
return eventService.countUserOnline(request, itemId);
} catch (CAServiceException e) {
e.printStackTrace();
LOG.error("========== Error when counting number of online user ==========" + e.getMessage());
}
return 0;
}
// Service
private static Map<String, Map<String, Integer>> numberOfCurrentViews;
private Thread countDownOnlineThread;
class OnlineCountingDownRunnable implements Runnable {
private List<String> timeoutList = new ArrayList<String>();
private void cleanTimeoutIps() {
for (String itemId : numberOfCurrentViews.keySet()) {
Map<String, Integer> currentIps = numberOfCurrentViews.get(itemId);
for(String ip : timeoutList){
currentIps.remove(ip);
}
}
}
@Override
public void run() {
try {
for (String itemId : numberOfCurrentViews.keySet()) {
Map<String, Integer> currentIps = numberOfCurrentViews.get(itemId);
for(String ip : currentIps.keySet()){
Integer timeout = new Integer(currentIps.get(ip).intValue() - 1);
if (timeout == 0) {
timeoutList.add(ip);
}
currentIps.put(ip, timeout);
}
}
cleanTimeoutIps();
// counting down time must be double increasing time
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
LOG.error("---------------- Thread error in counting down online user: " + e.getMessage());
}
}
}
public int countUserOnline(HttpServletRequest request, String itemId) throws CAServiceException {
// create a count down timer to detect if the user does not view the page anymore
String ip = request.getRemoteAddr();
// init counting down online user map
if (numberOfCurrentViews == null) {
numberOfCurrentViews = new ConcurrentHashMap<String, Map<String, Integer>>();
}
// start thread to check user online
if (countDownOnlineThread == null) {
countDownOnlineThread = new Thread(new OnlineCountingDownRunnable());
countDownOnlineThread.start();
}
LOG.debug("---------------- Requested IP: " + ip);
if (ip == null || ip.isEmpty()) {
throw new CAServiceException("======= Cannot detect Ip of the client =======");
}
if (numberOfCurrentViews.get(itemId) != null) {
Map<String, Integer> userView = numberOfCurrentViews.get(itemId);
if (userView.get(ip) != null) {
userView.put(ip, userView.get(ip).intValue() + 1);
} else {
userView.put(ip, 1);
}
numberOfCurrentViews.put(itemId, userView);
} else {
Map<String, Integer> ips = new ConcurrentHashMap<String, Integer>();
ips.put(ip, 1);
numberOfCurrentViews.put(itemId, ips);
}
LOG.debug(String.format(
"============= %s is seeing there is %s users viewing item %s =============",
ip, numberOfCurrentViews.get(itemId).size(), itemId
));
return numberOfCurrentViews.get(itemId).size();
}
问题
我不知道如何测试这个功能,因为它需要多个IP地址才能查看页面。我尝试过设置JMeter和像这个链接一样设置IP欺骗,但没有成功,所以我做了一个类似的小模拟测试来查看日志
@Test
public void testCountUserOnline() throws Exception {
List<HttpServletRequest> requests = new ArrayList<HttpServletRequest>();
for (int i = 0; i < 10; i ++) {
HttpServletRequest request = Mockito.mock(HttpServletRequest.class);
Mockito.when(request.getRemoteAddr()).thenReturn(String.format("192.168.1.%s", i));
requests.add(request);
}
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 10; i ++) {
Thread thread = new Thread(new RequestRunnable(requests.get(i)));
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
}
class RequestRunnable implements Runnable {
private HttpServletRequest request;
public RequestRunnable(HttpServletRequest request) {
this.request = request;
}
public void run() {
try {
int i = 0;
while (i < 10) {
eventService.countUserOnline(request, "1");
i++;
System.out.println(i);
Thread.sleep(1000);
}
} catch (CAServiceException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
但是,我对的实施没有那么大的信心
此外,这是人类用来统计页面上查看用户数量的正常方式吗?我只是想确保我不会错过任何东西,以防这部分有任何捷径。我正在使用Spring MVC 3.x
。我还需要支持IE 9:(((,所以Web Socket
不能广泛使用
我第一次实现这个时非常愚蠢。我不应该让计数器基于线程来使问题变得如此复杂。相反,我真正需要的只是在每个请求上保存timestamp
,然后创建一个线程,将当前时间与页面上请求的最后时间戳进行比较。如果时间差大于超时数(在这种情况下为10秒),则用户已经离开页面,然后,我只需要从Map
中删除该条目。
// make it as static, since this needs to be singleton
private static Map<String, Map<String, Date>> numberOfCurrentViews;
// make it as static, since this needs to be singleton
private static Thread cleaningOffLineUserThread;
class OnlineCountingDownRunnable implements Runnable {
@Override
public void run() {
try {
while(true) {
for (String itemId : numberOfCurrentViews.keySet()) {
Map<String, Date> userView = numberOfCurrentViews.get(itemId);
Iterator<Map.Entry<String, Date>> iterator = userView.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Date> views = iterator.next();
Date lastViewTime = views.getValue();
Date currentTime = new Date();
long seconds = (currentTime.getTime() - lastViewTime.getTime()) / 1000;
if (seconds > TIMEOUT) {
iterator.remove();
}
}
}
// make the cleaning worker running every 2 seconds
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
LOG.error("---------------- Thread error in counting down online user: " + e.getMessage());
}
}
}
public int countUserOnline(HttpServletRequest request, String itemId) throws CAServiceException {
// create a count down timer to detect if the user does not view the page anymore
String ip = request.getRemoteAddr();
// init counting down online user map
if (numberOfCurrentViews == null) {
numberOfCurrentViews = new ConcurrentHashMap<String, Map<String, Date>>();
}
// start thread to check user online
if (cleaningOffLineUserThread == null) {
cleaningOffLineUserThread = new Thread(new OnlineCountingDownRunnable());
cleaningOffLineUserThread.start();
}
LOG.debug("---------------- Requested IP: " + ip);
if (ip == null || ip.isEmpty()) {
throw new CAServiceException("======= Cannot detect Ip of the client =======");
}
Map<String, Date> userView;
if (numberOfCurrentViews.get(itemId) != null) {
userView = numberOfCurrentViews.get(itemId);
} else {
userView = new ConcurrentHashMap<String, Date>();
}
userView.put(ip, new Date());
numberOfCurrentViews.put(itemId, userView);
LOG.debug(String.format(
"============= %s is seeing there is %s users, %s viewing item %s =============",
ip, numberOfCurrentViews.get(itemId).size(),
String.valueOf(numberOfCurrentViews.get(itemId).keySet()), itemId
));
return numberOfCurrentViews.get(itemId).size();
}
我使用Mock测试来测试该功能,它通过日志很好地反映了结果
@Test
public void testCountUserOnline() throws Exception {
List<HttpServletRequest> requests = new ArrayList<HttpServletRequest>();
HttpServletRequest request1 = Mockito.mock(HttpServletRequest.class);
HttpServletRequest request2 = Mockito.mock(HttpServletRequest.class);
HttpServletRequest request3 = Mockito.mock(HttpServletRequest.class);
Mockito.when(request1.getRemoteAddr()).thenReturn(String.format("192.168.1.%s", 1));
Mockito.when(request2.getRemoteAddr()).thenReturn(String.format("192.168.1.%s", 2));
Mockito.when(request3.getRemoteAddr()).thenReturn(String.format("192.168.1.%s", 3));
requests.add(request1);
requests.add(request2);
requests.add(request3);
List<Thread> threads = new ArrayList<Thread>();
Thread thread1 = new Thread(new RequestRunnable(request1, 50, "1", "2"));
Thread thread2 = new Thread(new RequestRunnable(request2, 20, "1", "3"));
Thread thread3 = new Thread(new RequestRunnable(request3, 10, "3", "1"));
threads.add(thread1);
threads.add(thread2);
threads.add(thread3);
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
}
class RequestRunnable implements Runnable {
private HttpServletRequest request;
private String [] pageSet;
private int duration;
public RequestRunnable(HttpServletRequest request, int duration, String ... pageSet) {
this.request = request;
this.pageSet = pageSet;
this.duration = duration;
}
public void run() {
try {
int i = 0;
while(i < duration) {
i ++;
for (String page : pageSet) {
eventService.countUserOnline(request, page);
}
LOG.debug("Second " + i);
Thread.sleep(1000);
}
} catch (CAServiceException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
您尝试过Apache基准测试工具吗?
http://httpd.apache.org/docs/2.2/programs/ab.html