整理常见的网站在线人数统计方案

在日常的网站运营中,需要实时统计某个网站的在线人数,通过该指标来帮助运营人员更好地维护网站业务。下面就这个问题做一些实现方案的罗列。

1、监听session方式实现

实现原理:当浏览器访问页面的时候会产生一个session对象,当关闭该页面的时候会删除session对象。根据这个特点,每当产生一个新的session对象就让在线人数加1,当删除一人session对象就让在线人数减1。

核心代码的实现:

@WebListener 
public class MySessionListener implements HttpSessionListener{ 
   //记录当前在线d的总人数 
    private int onlineCount = 0; 
    /** 
     * session创建后执行 
     * sessionCreated是客户端第一次和服务器交互时触发 
     */ 
    @Override 
    public void sessionCreated(HttpSessionEvent se) { 
       //在线人数增加1 
        onlineCount++; 
        se.getSession().getServletContext().setAttribute("onlineCount", onlineCount); 
    } 
    /** 
     * session失效后执行 
     * 销毁会话的时候触发 
     */ 
    @Override 
    public void sessionDestroyed(HttpSessionEvent se) { 
        //在线人数减去1 
        if (onlineCount > 0) { 
            onlineCount--; 
        } 
        //将最新的onlineCount值存起来 
        se.getSession().getServletContext().setAttribute("onlineCount", onlineCount); 
    } 
}

监听session方式的虽然简单,但是存在如下的问题:

(1)存在统计不精准的问题。当用户关闭浏览器时并不会触发session监听,当下一次登录时仍然会让count加1;当session过期时,session监听不能做实时的响应将在线数减1。

(2)session方式容易被入侵者攻击;同时在高并发下,会导致服务器压力过大。

2、token方案

实现原理:用户携带账号和密码登录服务,服务器会生成一个带过期时间的token,然后返回token给客户端;当客户端要访问服务器的时候需要携带token过来,服务端判断token是否过期,如果没有过期就响应数据并且自动给token续期。如果token在一定时间内没有在请求服务器默认用户就下线了。为了统计方便统计在线的用户数量我们使用Map记录用户id和对用的续期时间。

核心的实现代码:

@Service 
public class OnLineManager{ 
#全局的map 记录在线用户 
  private static Map<Long,Long> onlineUserMap = new ConcurrentHashMap<>(); 
  /** 
   * 记录在线的用户 
   */ 
   public void recordOnLineUser(Long userId){ 
     long currentTime = System.currentTimeMillis(); 
     onlineUserMap.put(userId, currentTime + 60 * 1000); 
     } 
  /** 
   * 删除下线的用户 
   */ 
   public void deleteUser(Long userId){ 
     onlineUserMap.remove(userId); 
   } 

   /** 
    * 统计所有在线d的用户 
    */ 
    public Integer getonlineCount(){ 
      int onlineCount = 0; 
      Set<Long> userIdList = countMap.keySet() 
      long currentTime = System.currentTimeMillis(); 
      for (Long userId : userIdList) { 
      Long value = (Long) countMap.get(userId); 
      if (value > currentTime){ 
      onlineCount++; 
      return onlineCount; 
   }

本方案虽然可以统计有效的统计在线的人数,但是针对于一个日活上千万设置过亿的系统来说,我们最后记录用户数据的Map会非常的大,对系统不够友好。

3、Redis的Zset实现

实现原理:使用zset数据的score来实现,通过查询最近指定时间内不活跃的用户来定义用户是否在线,并且通过定时任务(如XXL-job)来清理redis中的中的“垃圾数据”,这样保证了读和存的效率。

核心代码:

@Component 
public class OnlineUserService { 

    private static final String ONLINE_USERS = "onlie_user_zset"; 
    @Resource 
    private StringRedisTemplate redisTemplate; 
    /** 
     * 添加用户在线信息 
     *   
     */ 
    public Boolean online(Integer userId) { 
        return redisTemplate.opsForZSet().add(ONLINE_USERS, userId.toString(), Instant.now().toEpochMilli()); 
    } 

    /** 
     * 获取在线的用户数量 
     *  
     */ 
    public Long count(Duration duration) { 
        LocalDateTime now = LocalDateTime.now(); 
        return redisTemplate.opsForZSet().count(ONLINE_USERS,  
                                    now.minus(duration).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),  
                                    now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); 
    } 

    /** 
     * 清除超过一定时间没在线的用户数据 
     *  
     */ 
    public Long clear(Duration duration) { 
        return redisTemplate.opsForZSet().removeRangeByScore(ONLINE_USERS, 0,  
                LocalDateTime.now().minus(duration).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); 
    } 
}
8