不能在中间件直接更新“在线状态”字段,因其将请求频次误等同于用户活跃度,导致db频繁写入、last_seen抖动大、无法区分真实活跃与爬虫刷量;应改用客户端心跳接口+redis过期键方案,由独立中间件在鉴权后轻量标记,在线判定依赖ttl自动淘汰。

为什么不能在中间件里直接更新“在线状态”字段
中间件每次请求都执行,但“在线”不是请求频次的函数——用户可能开着页面却 10 分钟没发请求,也可能 1 秒刷 5 次。直接在中间件里对数据库执行 UPDATE users SET last_seen = NOW() WHERE id = ? 会导致:频繁写入拖慢 DB、last_seen 时间抖动大、无法区分真实活跃与爬虫刷量。
用心跳接口 + Redis 过期键才是合理方案
真正的在线状态应由客户端主动保活,服务端只做轻量记录和查询。关键点是:不依赖中间件自动触发,而是拆出独立路由 + Redis TTL 控制。
- 前端每 30 秒调一次
/api/v1/heartbeat(带user_id或 token),后端仅执行redis.SetEx("online:user:123", "1", 60) - Redis key 过期时间设为 60 秒(略大于心跳间隔),自然淘汰离线用户
- 查在线数用
redis.Keys("online:user:*")不推荐——O(n) 扫描;改用redis.PubSub或更稳妥的redis.SCard("online:set")+redis.SAdd("online:set", "123") - 如果必须从中间件“顺手”埋点,只做轻量日志或指标打点(如 Prometheus counter),绝不做 DB 写或 Redis Set
JWT 中间件里怎么安全透传 user_id 做在线标记
别在 JWT 解析中间件里直接写 Redis——那会污染鉴权逻辑,且未通过鉴权的请求不该触发在线标记。正确做法是:鉴权成功后,在后续中间件或 handler 开头统一处理。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- JWT 中间件只负责解析并存
c.Set("user_id", id),用强类型 key(如const userIDKey userCtxKey = "user_id") - 单独挂一个
OnlineTracker()中间件,放在 JWT 之后、业务 handler 之前:func OnlineTracker() echo.MiddlewareFunc { return func(next echo.Handler) echo.Handler { return echo.HandlerFunc(func(c echo.Context) error { if id, ok := GetUserID(c); ok { redisClient.SetEx(c.Request().Context(), fmt.Sprintf("online:user:%d", id), "1", 60*time.Second) } return next.ServeHTTP(c) }) } } - 注意:该中间件必须在
Recover()之后,否则 panic 时不会执行;也别放Logger()前面,避免日志还没打就提前标记在线
查“当前在线用户列表”为什么不能直接查 Redis keys
redis.Keys("online:user:*") 在生产环境是危险操作:阻塞主线程、数据量大时超时、集群模式下不支持。真正可落地的方式只有两种:
- 用集合(Set)+ 定期清理:登录时
redis.SAdd("online:users", "123"),心跳续期用redis.Expire("online:user:123", 60),查总数用redis.SCard("online:users") - 用有序集合(ZSet)存
user_id → timestamp,用ZCount("online:zset", time.Now().Add(-60*time.Second).Unix(), "+inf")统计活跃窗口内数量 - 若需精确用户 ID 列表,必须配合定时任务(如每分钟扫一遍 ZSet range),不能在 HTTP 请求中实时拉取全量
线上最常被忽略的是 Redis 连接复用和上下文超时——redis.SetEx 调用必须传 c.Request().Context(),否则超时请求仍可能完成写入,导致“假在线”。










