一、为什么要统计用户在线时长?
在业务系统中,统计用户在线时长是一个非常常见的需求。不同的业务场景对在线时长有不同的理解,比如:
- IM系统:判断用户是否在线,及其活跃时长。
- 学习平台:统计用户每天的学习时间。
- 游戏系统:记录用户每日在线时长,用于反作弊或计算活跃奖励。
- SaaS系统:用于客户活跃度分析。
那如何设计一个灵活、可扩展、性能良好的“用户在线时长”系统呢?
二、业务场景分析
从业务角度来看,在线时长可以按以下几个维度进行统计:
- 按日统计:用户每天在线了多久。
- 按会话统计:每次登录-退出的在线时间。
- 实时在线状态:用户此刻是否在线。
- 跨设备:用户可能在多个设备登录。
此外,还要考虑:
- 网络断线、异常退出情况。
- 数据存储和后期分析。
三、技术选型
基于多年经验,我们可以从以下几个方案中选择:
1. 使用心跳机制 + Redis缓存
- 前端每隔30秒发送一次心跳请求。
- 使用Redis记录最后心跳时间。
- 判断“当前时间 - 最后心跳时间 < 超时时间”,则视为在线。
优点:实时性好,性能高
缺点:需要前端配合,断线后需要容错逻辑
2. 登录登出打点
- 记录用户登录时间和退出时间
- 每次会话都记录一条数据
优点:简单、易分析
缺点:断线等异常退出难处理
3. 混合方案(推荐)
- 登录登出打点 + 心跳补充
- 每天定时汇总Redis心跳日志 → MySQL
四、核心数据结构设计
我们使用Redis + MySQL结合的方案:
Redis结构示例
Key: online:user:{userId} Value: 时间戳(最后心跳时间) Type: String(或Hash) TTL: 5分钟自动过期
MySQL表结构设计
CREATE TABLE user_online_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, session_id VARCHAR(64), login_time DATETIME, logout_time DATETIME, duration_seconds INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
五、Java核心实现
1. 心跳接口
@RestController @RequestMapping("/api/online") public class OnlineController { private static final String ONLINE_KEY_PREFIX = "online:user:"; private static final long HEARTBEAT_EXPIRE_SECONDS = 300; // 5分钟 @Autowired private StringRedisTemplate redisTemplate; /** * 用户心跳接口 */ @PostMapping("/heartbeat") public ResponseEntity<String> heartbeat(@RequestParam Long userId) { String key = ONLINE_KEY_PREFIX + userId; redisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()), HEARTBEAT_EXPIRE_SECONDS, TimeUnit.SECONDS); return ResponseEntity.ok("heartbeat received"); } }
2. 登录/登出打点
@Service public class UserSessionService { @Autowired private UserOnlineLogRepository repository; private final Map<Long, UserOnlineLog> sessionMap = new ConcurrentHashMap<>(); /** * 用户登录,记录登录时间 */ public void login(Long userId, String sessionId) { UserOnlineLog log = new UserOnlineLog(); log.setUserId(userId); log.setSessionId(sessionId); log.setLoginTime(LocalDateTime.now()); sessionMap.put(userId, log); } /** * 用户登出,记录登出时间并计算在线时长 */ public void logout(Long userId) { UserOnlineLog log = sessionMap.remove(userId); if (log != null) { log.setLogoutTime(LocalDateTime.now()); long seconds = Duration.between(log.getLoginTime(), log.getLogoutTime()).getSeconds(); log.setDurationSeconds((int) seconds); repository.save(log); } } }
3. 定时任务汇总在线时长
@Component public class OnlineStatisticsJob { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserDailyOnlineRepository dailyRepository; @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点 public void collectDailyOnlineTime() { Set<String> keys = redisTemplate.keys("online:user:*"); if (keys == null) return; for (String key : keys) { Long userId = Long.valueOf(key.split(":")[2]); // 模拟一个5分钟的在线时长 UserDailyOnline online = new UserDailyOnline(); online.setUserId(userId); online.setDate(LocalDate.now().minusDays(1)); online.setDurationSeconds(300); // 实际应根据时间差统计 dailyRepository.save(online); } } }
六、总结与优化建议
- 心跳频率:建议30-60秒一次,平衡实时性与性能。
- Redis TTL机制:可自动判断用户是否掉线。
- 异常退出处理:通过定时任务弥补。
- 跨设备问题:Redis key可记录多个sessionId。
七、写在最后
用户在线时长看似简单,实则涉及多个技术点和业务判断。作为一个有8年经验的Java开发者,我建议:从业务需求出发,结合Redis的高性能与MySQL的持久化能力,构建一个稳定、可扩展的在线时长系统。
欢迎评论区交流你们的实现方案!

5910

被折叠的 条评论
为什么被折叠?



