整理常见的网站在线人数统计方案
在日常的网站运营中,需要实时统计某个网站的在线人数,通过该指标来帮助运营人员更好地维护网站业务。下面就这个问题做一些实现方案的罗列。
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());
}
}