Go-Zero 项目开发26: 实现用户在线离线状态维护 纲要业务需求分析好友在线状态群成员在线状态状态变更的感知机制方案设计基于Redis的集中式状态存储登录退出时更新Hash缓存查询在线状态先获取关系列表再批量查缓存状态变更通知方案对比WebSocket推送 vs 客户端轮询/按需拉取代码实现项目结构介绍配置Redis连接与常量定义登录成功后写入在线状态好友在线状态查询接口群成员在线状态查询接口测试与验证总结用户在线状态业务分析在即时通讯IM系统中用户在线状态是最基础的功能之一。当用户登录后好友列表中对应的头像会从灰色变为彩色表示上线退出或断开连接后恢复灰色表示离线。看似简单的功能背后却涉及登录状态记录、关系链查询、状态变更通知等多方面设计。好友状态好友在线状态的核心点包括用户登录退出时的处理服务器需要记录当前在线用户并在用户离线时及时清除记录。获取全部好友在线状态用户登录后应能够立即知晓所有好友的在线情况。状态变更传播用户 A 的好友 B 状态发生变化时A 需要感知到。群成员状态群成员状态的复杂性更高群数量多、成员规模大无法直接套用好友的实时推送方式。用户不会同时关注所有群实际活跃的仅有少数几个。更适合采用按需拉取或延迟拉取在用户打开某个群会话时才去获取该群成员的在线状态并可设置一定时间间隔进行增量刷新。技术选型与方案设计采用Go-Zero微服务框架构建将用户状态数据存放于Redis选择Hash数据结构Key: 固定常量如user:online:status代表整套在线状态哈希表。Field: 用户ID。Value: 状态信息或标志位如1表示在线可扩展为设备信息等。这样设计的好处是查询单个或批量用户状态时非常高效利用HMGET或HGETALL即可。登录退出时的状态维护流程RedisUser RPCAPI 服务客户端RedisUser RPCAPI 服务客户端登录请求登录验证登录成功返回用户IDHSET user:online:status {userId} 1登录成功退出请求HDEL user:online:status {userId}退出成功好友在线状态查询流程用户登录后API 服务需要返回所有好友的在线状态调用社交服务获取好友ID列表。批量查询Redis中user:online:status的对应字段。组装结果返回。RedisSocial RPCAPI 服务客户端RedisSocial RPCAPI 服务客户端查询好友在线状态获取好友列表 (userId)好友ID列表HMGET user:online:status id1 id2 ...在线状态列表好友在线状态 Map群成员在线状态查询流程与好友类似但查询的是指定群的成员ID同样通过社交服务获取成员列表然后批量查Redis。客户端在打开群会话时触发请求可配合定时器按需刷新。代码实现项目结构假设项目名为im包含api网关、rpc用户服务、社交服务等模块。与在线状态相关的改动主要集中在api层的业务逻辑和配置文件。im/ ├── api/ │ ├── etc/ │ │ └── im-api.yaml │ ├── internal/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── logic/ │ │ │ ├── loginlogic.go │ │ │ ├── friendsonlinelogic.go │ │ │ └── groupmemberonlinelogic.go │ │ ├── svc/ │ │ │ └── servicecontext.go │ │ └── types/ │ │ └── types.go │ ├── im.api │ └── im.go ├── rpc/ │ ├── user/ │ └── social/ └── go.mod配置文件与依赖注入在im-api.yaml中添加 Redis 配置Name:im-apiHost:0.0.0.0Port:8888Redis:Host:127.0.0.1:6379Pass:Type:nodeUserRpc:Etcd:Hosts:-127.0.0.1:2379Key:user.rpcSocialRpc:Etcd:Hosts:-127.0.0.1:2379Key:social.rpc在internal/config/config.go中定义对应的配置结构体packageconfigimport(github.com/zeromicro/go-zero/core/stores/redisgithub.com/zeromicro/go-zero/zrpc)typeConfigstruct{rest.RestConf Redis redis.RedisConf UserRpc zrpc.RpcClientConf SocialRpc zrpc.RpcClientConf}在internal/svc/servicecontext.go中初始化 Redis 和 RPC 客户端packagesvcimport(github.com/zeromicro/go-zero/core/stores/redisgithub.com/zeromicro/go-zero/zrpcim/api/internal/configim/rpc/user/userclientim/rpc/social/socialclient)typeServiceContextstruct{Config config.Config Redis*redis.Redis UserRpc userclient.User SocialRpc socialclient.Social}funcNewServiceContext(c config.Config)*ServiceContext{returnServiceContext{Config:c,Redis:redis.MustNewRedis(c.Redis),UserRpc:userclient.NewUser(zrpc.MustNewClient(c.UserRpc)),SocialRpc:socialclient.NewSocial(zrpc.MustNewClient(c.SocialRpc)),}}定义常量在internal/consts/consts.go或直接放在 logic 层中定义Redis在线状态 KeypackageconstsconstUserOnlineStatusKeyuser:online:status登录成功后设置在线状态假设登录逻辑在internal/logic/loginlogic.go修改登录成功后的处理packagelogicimport(contextfmtim/api/internal/constsim/api/internal/svcim/api/internal/typesim/rpc/user/userclientgithub.com/zeromicro/go-zero/core/logx)typeLoginLogicstruct{logx.Logger ctx context.Context svcCtx*svc.ServiceContext}funcNewLoginLogic(ctx context.Context,svcCtx*svc.ServiceContext)*LoginLogic{returnLoginLogic{Logger:logx.WithContext(ctx),ctx:ctx,svcCtx:svcCtx,}}func(l*LoginLogic)Login(req*types.LoginReq)(resp*types.LoginResp,errerror){// 调用用户 RPC 进行登录验证loginResp,err:l.svcCtx.UserRpc.Login(l.ctx,userclient.LoginRequest{Username:req.Username,Password:req.Password,})iferr!nil{returnnil,err}userId:loginResp.UserId// 登录成功将用户 ID 写入 Redis 在线状态哈希表_,errl.svcCtx.Redis.Hset(consts.UserOnlineStatusKey,fmt.Sprintf(%d,userId),1)iferr!nil{l.Errorf(set user online status failed: %v,err)// 写入失败可根据业务决定是否让登录失败这里仅记录日志并允许登录继续}returntypes.LoginResp{UserId:userId,Token:loginResp.Token,},nil}**退出登录**的处理类似调用HDEL移除字段即可gofunc(l*LogoutLogic)Logout(req*types.LogoutReq)error{_,err:l.svcCtx.Redis.Hdel(consts.UserOnlineStatusKey,fmt.Sprintf(%d,req.UserId))returnerr}好友在线状态查询接口在im.api文件中定义请求/响应类型和接口// 好友在线状态查询无需请求参数从 token 中获取当前用户 IDserver(jwt:Auth)get/friends/online(FriendsOnlineReq)returns(FriendsOnlineResp)对应types/types.gotypeFriendsOnlineReqstruct{}typeFriendsOnlineRespstruct{OnlineStatusmap[int64]booljson:onlineStatus// key: 好友 ID, value: 是否在线}在internal/logic/friendsonlinelogic.go中实现packagelogicimport(contextfmtim/api/internal/constsim/api/internal/svcim/api/internal/typesim/rpc/social/socialclientgithub.com/zeromicro/go-zero/core/logx)typeFriendsOnlineLogicstruct{logx.Logger ctx context.Context svcCtx*svc.ServiceContext}funcNewFriendsOnlineLogic(ctx context.Context,svcCtx*svc.ServiceContext)*FriendsOnlineLogic{returnFriendsOnlineLogic{Logger:logx.WithContext(ctx),ctx:ctx,svcCtx:svcCtx,}}func(l*FriendsOnlineLogic)FriendsOnline(req*types.FriendsOnlineReq)(resp*types.FriendsOnlineResp,errerror){// 从上下文中获取当前登录用户 ID由 JWT 中间件注入userId:l.ctx.Value(userId).(int64)// 1. 调用社交服务获取好友 ID 列表friendsResp,err:l.svcCtx.SocialRpc.GetFriendList(l.ctx,socialclient.GetFriendListReq{UserId:userId,})iferr!nil{returnnil,err}friendIds:friendsResp.FriendIdsiflen(friendIds)0{returntypes.FriendsOnlineResp{OnlineStatus:map[int64]bool{}},nil}// 2. 构建 Redis HMGET 所需的字段列表fields:make([]string,len(friendIds))fori,id:rangefriendIds{fields[i]fmt.Sprintf(%d,id)}// 3. 批量查询在线状态vals,err:l.svcCtx.Redis.Hmget(consts.UserOnlineStatusKey,fields...)iferr!nil{returnnil,err}// 4. 组装结果onlineStatus:make(map[int64]bool,len(friendIds))fori,id:rangefriendIds{onlineStatus[id]vals[i]!}returntypes.FriendsOnlineResp{OnlineStatus:onlineStatus},nil}群成员在线状态查询接口与好友在线状态类似但需要传入群 ID并调用社交服务获取群成员列表。定义 API 和类型// im.api 中增加server(jwt:Auth)post/group/members/online(GroupMembersOnlineReq)returns(GroupMembersOnlineResp)// types.gotypeGroupMembersOnlineReqstruct{GroupIdint64json:groupId}typeGroupMembersOnlineRespstruct{OnlineStatusmap[int64]booljson:onlineStatus}Logic 实现packagelogicimport(contextfmtim/api/internal/constsim/api/internal/svcim/api/internal/typesim/rpc/social/socialclient)typeGroupMembersOnlineLogicstruct{logx.Logger ctx context.Context svcCtx*svc.ServiceContext}funcNewGroupMembersOnlineLogic(ctx context.Context,svcCtx*svc.ServiceContext)*GroupMembersOnlineLogic{returnGroupMembersOnlineLogic{Logger:logx.WithContext(ctx),ctx:ctx,svcCtx:svcCtx,}}func(l*GroupMembersOnlineLogic)GroupMembersOnline(req*types.GroupMembersOnlineReq)(resp*types.GroupMembersOnlineResp,errerror){// 调用社交服务获取群成员 ID 列表membersResp,err:l.svcCtx.SocialRpc.GetGroupMembers(l.ctx,socialclient.GetGroupMembersReq{GroupId:req.GroupId,})iferr!nil{returnnil,err}memberIds:membersResp.MemberIdsiflen(memberIds)0{returntypes.GroupMembersOnlineResp{OnlineStatus:map[int64]bool{}},nil}// 构建字段列表HMGET 查询fields:make([]string,len(memberIds))fori,id:rangememberIds{fields[i]fmt.Sprintf(%d,id)}vals,err:l.svcCtx.Redis.Hmget(consts.UserOnlineStatusKey,fields...)iferr!nil{returnnil,err}onlineStatus:make(map[int64]bool,len(memberIds))fori,id:rangememberIds{onlineStatus[id]vals[i]!}returntypes.GroupMembersOnlineResp{OnlineStatus:onlineStatus},nil}测试与验证启动所有服务后使用curl或 Postman 进行测试。登录用户 A返回userId: 1001。调用GET /friends/online查看好友在线状态此时可能全部离线。使用另一个账号 BuserId: 1002登录再次调用 A 的好友在线状态接口可看到 B 变为在线。退出 B再次查询B 变为离线。使用群成员在线接口POST /group/members/online传入groupId获得该群成员的在线情况。测试过程中需确保Redis正常运行且 RPC 服务返回正确的数据。总结利用Go-Zero微服务框架和RedisHash 结构我们可以高效地实现 IM 系统的用户在线状态维护。该方案的优势在于低延迟全部操作基于内存缓存查询批量在线状态只需一次HMGET。架构清晰状态存储与业务逻辑解耦API 层通过 RPC 获取关系链数据再与缓存交互。易扩展Hash的字段值可以扩展为 JSON记录设备类型、登录时间等丰富信息。对于状态变更的实时性要求可进一步引入WebSocket或Server-Sent Events进行推送在群聊场景中结合按需拉取能够很好地平衡性能与用户体验。