如何统计在线用户数(基于 Session)?需要注意什么?

张开发
2026/6/8 18:21:08 15 分钟阅读
如何统计在线用户数(基于 Session)?需要注意什么?
如何优雅地统计在线用户数从单机到分布式的最佳实践引言一个图书馆的访客计数器一、前置知识HTTP 无状态与 Session1.1 HTTP 为什么“健忘”1.2 Session让服务器“记住”用户二、核心场景什么是在线用户三、单机版实现HttpSessionListener AtomicInteger3.1 核心原理3.2 为什么用 AtomicInteger 而不是 int3.3 代码实现3.4 配置监听器Servlet 3.03.5 单机方案的局限性四、分布式演进Redis 全局计数器4.1 问题单机计数为何失效4.2 解决方案Redis 原子操作4.3 改造后的代码4.4 单机 vs 分布式方案对比4.5 关键注意事项Session 过期事件的挑战五、生产环境避坑指南5.1 坑一只加不减导致数据虚高5.2 坑二统计了所有 Session包括“未登录游客”5.3 坑三并发安全问题单机版5.4 坑四集群环境下每个节点独立计数六、总结引言一个图书馆的访客计数器想象你经营一家图书馆。每天开馆时你需要知道馆内实时有多少读者。你在入口放一个计数器每位读者进入时按一下“1”离开时按一下“-1”。这个计数器很简单也很准确。但如果图书馆开了分馆呢每个分馆都有自己的计数器你无法知道整个集团的总读者数。这时你需要在所有分馆共享一个“总计数器”。在 Web 世界里在线用户数就是这样一个“实时计数器”。本文将带你从单机到分布式一步步构建优雅的在线用户统计方案。一、前置知识HTTP 无状态与 Session1.1 HTTP 为什么“健忘”HTTP 协议在设计之初是无状态的——服务器不会记住两次请求之间的关系。你第一次访问网站时登录了第二次请求时服务器已经忘了你是谁。1.2 Session让服务器“记住”用户为了解决这个问题Web 容器引入了Session机制用户首次访问时服务器创建一个 Session 对象生成唯一 Session ID。服务端将 Session ID 通过 Cookie 返回给浏览器。后续请求浏览器自动携带该 Cookie服务器根据 Session ID 找到对应的 Session 数据。一个 Session 就是一个“在线用户”通常如此下文会讨论特殊情况。二、核心场景什么是在线用户在开始统计之前我们需要明确“在线用户”的界定严格定义当前存在活跃 Session 的用户。常见业务定义已登录的用户Session 中包含用户信息最近 N 分钟内有操作的用户基于最后访问时间本文默认以 Session 存活为基准统计当前活跃的 Session 数量。三、单机版实现HttpSessionListener AtomicInteger3.1 核心原理Servlet 容器提供了HttpSessionListener接口可以监听 Session 的创建和销毁事件sessionCreated()Session 创建时触发用户首次访问、登录等sessionDestroyed()Session 销毁时触发超时、主动登出、服务器关闭我们只需要在监听器中维护一个全局计数器即可。3.2 为什么用AtomicInteger而不是int多线程环境下多个请求可能同时创建/销毁 Session普通的int自增操作非线程安全会导致计数不准。AtomicInteger通过 CAS比较并交换保证原子性。3.3 代码实现importjavax.servlet.annotation.WebListener;importjavax.servlet.http.HttpSessionEvent;importjavax.servlet.http.HttpSessionListener;importjava.util.concurrent.atomic.AtomicInteger;WebListenerpublicclassOnlineUserListenerimplementsHttpSessionListener{// 全局在线用户计数器线程安全privatestaticfinalAtomicIntegeronlineCountnewAtomicInteger(0);OverridepublicvoidsessionCreated(HttpSessionEventse){intcurrentonlineCount.incrementAndGet();System.out.println(用户上线当前在线人数current);}OverridepublicvoidsessionDestroyed(HttpSessionEventse){intcurrentonlineCount.decrementAndGet();System.out.println(用户下线当前在线人数current);}// 提供对外访问的接口供其他业务查询publicstaticintgetOnlineCount(){returnonlineCount.get();}}3.4 配置监听器Servlet 3.0如果使用 Spring Boot可以通过Bean方式注册ConfigurationpublicclassSessionConfig{BeanpublicHttpSessionListenerhttpSessionListener(){returnnewOnlineUserListener();}}验证启动应用打开多个浏览器窗口访问观察控制台输出。3.5 单机方案的局限性优点缺点实现简单无外部依赖无法跨节点共享集群环境下每个节点计数独立性能极高内存操作重启丢失应用重启后计数器归零准确可靠不支持分布式扩展当系统从单机演变为多节点集群时这个方案就会失效。四、分布式演进Redis 全局计数器4.1 问题单机计数为何失效在集群环境下用户的请求可能被负载均衡到不同节点用户在 Node A 登录Node A 的计数器 1。用户在 Node B 主动登出Node B 的计数器 -1但 Node A 的计数器没变。总在线人数 Node A Node B但真实用户只有一个。结论集群下需要全局统一存储来维护计数。4.2 解决方案Redis 原子操作Redis 提供了INCR和DECR原子命令天然支持分布式计数。架构图Node A ───┐ Node B ───┼── Redis (counter) Node C ───┘4.3 改造后的代码ComponentpublicclassDistributedOnlineUserListenerimplementsHttpSessionListener{AutowiredprivateRedisTemplateString,StringredisTemplate;privatestaticfinalStringONLINE_COUNT_KEYonline_count;OverridepublicvoidsessionCreated(HttpSessionEventse){// 使用 Redis 的 INCR 原子递增LongcountredisTemplate.opsForValue().increment(ONLINE_COUNT_KEY);System.out.println(用户上线当前在线人数count);}OverridepublicvoidsessionDestroyed(HttpSessionEventse){// 使用 Redis 的 DECR 原子递减LongcountredisTemplate.opsForValue().decrement(ONLINE_COUNT_KEY);System.out.println(用户下线当前在线人数count);}publicstaticLonggetOnlineCount(RedisTemplateString,StringredisTemplate){StringcountredisTemplate.opsForValue().get(ONLINE_COUNT_KEY);returncountnull?0L:Long.parseLong(count);}}4.4 单机 vs 分布式方案对比维度单机方案 (AtomicInteger)分布式方案 (Redis)数据共享❌ 无法跨节点✅ 全局共享性能极快纳秒级快毫秒级内网可靠性重启丢失✅ 支持持久化重启不丢复杂度低中等需维护 Redis适用场景单机部署、开发测试生产环境、集群部署4.5 关键注意事项Session 过期事件的挑战在分布式环境下Session 过期事件的处理需要特别关注问题Session 超时后容器会触发sessionDestroyed事件。但在集群中Session 可能是在 Node A 创建的但超时事件只在 Node A 触发。如果 Node A 宕机这个 Session 永远不会触发销毁事件导致 Redis 计数器“只增不减”。解决方案利用 Redis 本身的过期特性不依赖 Session 销毁事件改用Redis 键过期通知。每次创建 Session 时在 Redis 中记录一个临时键如session:active:{sessionId}并设置与 Session 相同的过期时间。当该键过期时Redis 会发布通知监听该事件并递减计数器。定时扫描兜底定期扫描所有活跃 Session与 Redis 计数对比修正。五、生产环境避坑指南5.1 坑一只加不减导致数据虚高原因Session 超时未触发销毁如节点宕机用户直接关闭浏览器不触发销毁Session 仍在解决结合 Redis 键过期 定时扫描兜底双重保障。5.2 坑二统计了所有 Session包括“未登录游客”问题用户只是打开首页未登录也会创建 Session统计进“在线用户”这不合理。解决只统计“已登录”的 Session。OverridepublicvoidsessionCreated(HttpSessionEventse){// 先不计数等待用户登录}publicvoiduserLogin(HttpSessionsession){session.setAttribute(isLoggedIn,true);redisTemplate.opsForValue().increment(ONLINE_COUNT_KEY);}OverridepublicvoidsessionDestroyed(HttpSessionEventse){HttpSessionsessionse.getSession();BooleanisLoggedIn(Boolean)session.getAttribute(isLoggedIn);if(isLoggedIn!nullisLoggedIn){redisTemplate.opsForValue().decrement(ONLINE_COUNT_KEY);}}5.3 坑三并发安全问题单机版问题直接用int作为计数器高并发下出现数据错乱。解决单机用AtomicInteger分布式用 Redis 原子操作。5.4 坑四集群环境下每个节点独立计数问题A 节点统计 100B 节点统计 100总在线显示 200但实际只有 100 个用户。解决必须使用 Redis 等外部存储统一计数。六、总结场景推荐方案核心组件单机开发/测试HttpSessionListenerAtomicInteger内存计数器生产集群推荐HttpSessionListener RedisRedisINCR/DECR高精度兜底上述方案 Redis 键过期通知 定时扫描双重保障机制核心思想统计的本质是对“在线”这个状态的实时计数单机时状态在本地分布式时状态必须外移到共享存储Redis永远不要忽略异常情况节点宕机、Session 未正常销毁用过期通知和定时任务兜底在线用户数看起来是一个小功能却折射出分布式系统设计的核心思想状态外移、原子操作、异常兜底。掌握它你就掌握了分布式计数的基础。toc](

更多文章