用gin中间件记录用户在线状态需基于请求上下文轻量标记,解析token获取userid后写入redis有序集合online_users,zadd存时间戳、zcount秒级查询,lua脚本定时清理过期成员,并校验认证信息避免未登录用户计入统计。

如何用 Gin 的中间件记录用户在线状态
在线状态不是靠“心跳包”实时推,而是基于请求上下文做轻量标记。Gin 本身不维护连接,所以得在每次有效请求里更新用户最后活跃时间——关键不是“在线”,而是“最近一次被观测到的时间”。
推荐用 context.WithValue + 全局 map(或 Redis)组合:中间件解析 token 或 session 获取 userID,然后写入共享存储。注意别直接把 time.Now() 存进 context 传给后续 handler,那只是单次请求快照,没统计意义。
- 用
sync.Map适合小规模( - 若部署多实例,必须切到 Redis,用
SET user:123:online "1" EX 300配合过期时间 - 不要在中间件里做耗时操作(如查 DB),否则拖慢所有接口;先写缓存,异步落库
为什么不能依赖 WebSocket 连接数判断“在线”
很多人误以为启了 WebSocket 就能精确知道谁在线,但 Gin 默认不处理 WebSocket 升级,且真实场景中:页面关闭、网络中断、浏览器休眠都会让连接静默断开,而服务端可能几十秒后才收到 FIN 包甚至收不到。TCP keepalive 默认是 2 小时,完全不可靠。
真正能信的只有“最近一次 HTTP 请求时间”,配合合理过期策略(比如 5 分钟没新请求就视为离线)。
- Gin 原生不支持 WS,要用
gorilla/websocket手动升级,且每个连接需单独维护生命周期 - WS 连接数 ≠ 在线用户数:一个用户开多个标签页会建多个连接;刷新页面也会重建
- 如果硬要用 WS 上报在线,务必加客户端定时 ping(
setInterval(() => ws.send("ping"), 20000)),服务端记录最后一次 pong 时间
统计接口怎么设计才不拖垮数据库
高频更新 + 低频查询,必须分层。别让 SELECT COUNT(*) FROM users WHERE last_active > NOW() - INTERVAL 5 MINUTE 直接扫表——尤其当用户表上亿时,这句 SQL 会堵死整个 DB。
正确做法是:写缓存时顺手更新聚合值,查的时候直接取。
- 用 Redis 的
ZADD online_users <timestamp> userID</timestamp>存有序集合,ZCOUNT online_users (now-300) +inf秒级响应 - 每分钟用 Lua 脚本清理过期成员:
ZREMRANGEBYSCORE online_users 0 <now-300></now-300>,避免内存膨胀 - 如果必须落库做审计,走异步任务(如 Golang 的
time.AfterFunc延迟 5 分钟写一次汇总记录),而非每次请求都 INSERT
常见错误:并发更新导致状态丢失
多个请求几乎同时到达,都读到旧的 last_active,各自加 1 再写回,结果只生效一次。这是典型的竞态条件。
Redis 的 SET key value EX 300 NX 只适用于首次设置;对于“更新最后活跃时间”,得用原子命令:
-
SET user:123:last_active "1717024567" EX 300—— 每次覆盖,天然幂等 - 或者用
EXPIRE user:123:last_active 300配合GETSET,但不如前者简洁 - 绝对不要用 Go 的
map[userID]time.Time在多 goroutine 下直接赋值,sync.Map的Store是安全的,但记得 key 类型统一为string或int64,别混用
最易被忽略的一点:前端没带认证信息(比如未登录用户访问首页)也要考虑。这类请求不该计入在线统计,中间件里得先判断 userID != 0 或 token != "",再决定是否更新状态。











