用String类型存心跳时间戳最直接,每个用户一个key,值为毫秒时间戳,如user:1001:last_active→"1729316400123";查在线用户时判断当前时间减时间戳≤300000ms;set必须带EX防雪崩;过期时间设为心跳间隔2–3倍;禁用KEYS,改用SCAN模糊匹配;前后端心跳频率与判定阈值需对齐。

用 String 类型存心跳时间戳最直接
不需要复杂结构,每个用户一个 key,值就是最后一次心跳的毫秒时间戳,比如 user:1001:last_active → "1729316400123"。服务端查在线用户时,只取当前时间减去该时间戳 ≤ 5 分钟(300000ms)的用户即可。
这种方式读写快、逻辑清晰、调试方便,适合中小并发量场景。避免一上来就用 ZSET 或 HASH 增加理解成本和误用风险。
set 命令必须带 EX 参数防雪崩
单纯用 stringRedisTemplate.opsForValue().set(key, value) 不设过期时间,一旦客户端停止发心跳,key 就永远滞留,最终撑爆 Redis 内存。
正确做法是每次心跳都带自动过期:
stringRedisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()), 5, TimeUnit.MINUTES);
注意三点:
- 过期时间建议设为心跳间隔的 2–3 倍(如前端每 10s 心跳,后端设 30s 过期),避免网络抖动误判离线
- 不要依赖客户端主动删 key,所有状态必须服务端可控
- 若用
RedisTemplate而非StringRedisTemplate,记得序列化器配置一致,否则get()可能返回null
查询在线用户别用 KEYS,改用 SCAN + 模糊匹配
KEYS online:user:* 在生产环境是高危操作,会阻塞 Redis 主线程,数据量大时可能触发超时熔断。
应改用非阻塞的 SCAN:
Set<String> keys = stringRedisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> result = new HashSet<>();
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
.match("online:user:*").count(1000).build());
while (cursor.hasNext()) {
result.add(new String(cursor.next()));
}
return result;
});再对每个 key GET 时间戳并比对是否在有效窗口内。虽然多一次网络往返,但安全可控。
前端心跳频率和后端容忍阈值要对齐
常见坑是前后端“心率”不一致:前端按 30s 发一次,后端却用 60s 判定离线,结果用户切后台 45s 后就被踢下线;或反过来,前端 10s 心跳,后端却只设 15s 过期,弱网下极易误判。
实操建议:
- 前端定时器用
setInterval(() => api.heartbeat(), 10000)(10s) - 后端
set(..., 30, TimeUnit.SECONDS)(30s 过期) - 判定在线逻辑用
now - lastActive ,而非依赖 key 是否存在 - 如果用 WebSocket,心跳包可复用连接通道,不必额外 HTTP 请求
真正难的不是写几行 set 和 get,而是把「心跳间隔」「Redis 过期时间」「服务端判定阈值」这三个数在文档里写死、在配置里对齐、在监控里验证——漏掉任意一环,线上就会出现“人还开着页面,状态却显示离线”的问题。


















