如何设计用户在线时长统计系统?

184 阅读4分钟

沉默是金,总会发光

大家好,我是沉默

在做架构设计的这几年里,我遇到过无数奇奇怪怪的需求,但有一个需求看似简单,实际却“暗藏杀机”—— 统计用户在线时长

很多开发者第一次接到这个需求时,心里想的可能是:

“不就是记下登录时间和退出时间,做个差就完了吗?”

但真上手后你会发现:

  • 网络抖动、掉线重连怎么算?

  • APP直接杀进程,退出时间怎么统计?

  • 多端同时在线,时长要不要合并?

  • 数据要实时还是只要离线统计?

这些问题如果没想清楚,就会在上线后被打爆工单。

我作为一个写了10年 Java 的老码农,也曾在这个需求上踩过不少坑。

这篇文章,我将从业务场景、技术选型、数据结构到核心实现,完整拆解一个高性能、可扩展的用户在线时长统计方案,希望能帮你少走弯路。

**-**01-

为什么要统计用户在线时长?

在不同的业务系统里,用户在线时长几乎是一个标配指标:

  • IM系统:判断用户是否在线,以及累计活跃时长。

  • 学习平台:统计用户每天的学习时长,作为学习效果的重要依据。

  • 游戏系统:记录每日在线时长,用于反作弊或计算活跃奖励。

  • SaaS系统:作为客户活跃度分析的关键数据指标。

但“在线时长”并不是单一维度,不同场景下含义不同:

场景统计粒度难点
日活分析按天统计如何高效汇总?
会话管理登录 - 登出时长异常退出难处理
实时状态当前是否在线需要低延迟感知
跨设备多端同时在线如何合并时长?

- 02-

业务场景分析

我们可以把用户在线时长的需求,拆解成几个维度:

  1. 按日统计:用户每天在线多久?

  2. 按会话统计:一次完整登录-退出的在线时长。

  3. 实时在线状态:此刻用户是否在线?

  4. 跨设备支持:同一用户多设备在线的合并策略。

还要处理几个棘手问题:

  • 网络波动导致断线重连

  • APP/浏览器异常关闭

  • 服务端扩容时的多节点统计

- 03-

技术方案对比

结合实际经验,常见有三种实现思路:

方案核心思路优点缺点适用场景
心跳机制 + Redis前端定时上报心跳,Redis记录最后时间戳实时性好,性能高依赖前端,断线容错复杂IM、游戏
登录/登出打点记录登录时间、登出时间简单易分析异常退出时数据不准SaaS、学习平台
混合方案(推荐)登录登出打点 + 心跳补偿 + 定时汇总兼顾实时性和准确性实现稍复杂绝大多数业务系统

从经验看,混合方案最稳妥:

  • Redis 做实时缓存和心跳状态

  • MySQL 做持久化和统计汇总

**-**04-

实战案例

核心数据结构设计

  1. Redis 结构
Key: online:user:{userId}:{sessionId}Value: 时间戳(最后心跳时间)TTL: 5分钟自动过期

TTL自动过期,可以自然判断用户是否掉线。

  1. 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. 心跳接口
@PostMapping("/heartbeat")public ResponseEntity<String> heartbeat(@RequestParam Long userId) {    String key = "online:user:" + userId;    redisTemplate.opsForValue().set(        key,        String.valueOf(System.currentTimeMillis()),        300, TimeUnit.SECONDS    );    return ResponseEntity.ok("heartbeat received");}

2. 登录/登出打点

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. 定时任务(每日统计)

@Scheduled(cron = "0 0 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]);        UserDailyOnline online = new UserDailyOnline();        online.setUserId(userId);        online.setDate(LocalDate.now().minusDays(1));        online.setDurationSeconds(300); // 示例:实际需统计心跳差值        dailyRepository.save(online);    }}

**-**05-

总结

如何选择方案?

图片

优化建议

  • 心跳频率:30~60秒一次,平衡实时性和性能。

  • Redis TTL:让异常掉线自动过期,避免冗余数据。

  • 异常退出:用定时任务补偿,避免漏算时长。

  • 跨设备:Redis key 里带 sessionId,再做合并。

最后

用户在线时长设计,看似是一个小需求,但涉及 实时计算、异常容错、跨设备合并、数据持久化 等多个技术点。

作为一名有10年经验的 Java 开发者,我的建议是:
先满足业务需求,再兼顾扩展性和性能,别一上来就过度设计。

如果你也踩过在线时长的坑,或者有更巧妙的实现方式,欢迎在评论区分享交流

**-**06-

粉丝福利

点点关注,送你 DeepSeek 全部资料,如果你正在室使用 DeepSeek,又或者刚准备学习 AI 大模型。可以仔细阅读一下,或许对你有所帮助!

图片