Go-Zero 项目开发26: 实现用户在线离线状态维护

纲要

  • 业务需求分析
    • 好友在线状态
    • 群成员在线状态
    • 状态变更的感知机制
  • 方案设计
    • 基于 Redis 的集中式状态存储
    • 登录/退出时更新 Hash 缓存
    • 查询在线状态:先获取关系列表,再批量查缓存
    • 状态变更通知方案对比(WebSocket 推送 vs 客户端轮询/按需拉取)
  • 代码实现
    • 项目结构介绍
    • 配置 Redis 连接与常量定义
    • 登录成功后写入在线状态
    • 好友在线状态查询接口
    • 群成员在线状态查询接口
  • 测试与验证
  • 总结

用户在线状态业务分析

在即时通讯(IM)系统中,用户在线状态是最基础的功能之一。当用户登录后,好友列表中对应的头像会从灰色变为彩色,表示上线;退出或断开连接后恢复灰色,表示离线。看似简单的功能,背后却涉及登录状态记录、关系链查询、状态变更通知等多方面设计。

好友状态

好友在线状态的核心点包括:

  • 用户登录/退出时的处理:服务器需要记录当前在线用户,并在用户离线时及时清除记录。
  • 获取全部好友在线状态:用户登录后,应能够立即知晓所有好友的在线情况。
  • 状态变更传播:用户 A 的好友 B 状态发生变化时,A 需要感知到。

群成员状态

群成员状态的复杂性更高:

  • 群数量多、成员规模大,无法直接套用好友的实时推送方式。
  • 用户不会同时关注所有群,实际活跃的仅有少数几个。
  • 更适合采用按需拉取延迟拉取:在用户打开某个群会话时,才去获取该群成员的在线状态;并可设置一定时间间隔进行增量刷新。

技术选型与方案设计

采用 Go-Zero 微服务框架构建,将用户状态数据存放于 Redis,选择 Hash 数据结构:

  • Key : 固定常量,如 user:online:status(代表整套在线状态哈希表)。
  • Field: 用户ID。
  • Value : 状态信息或标志位(如 1 表示在线,可扩展为设备信息等)。

这样设计的好处是查询单个或批量用户状态时非常高效,利用 HMGETHGETALL 即可。

登录/退出时的状态维护流程

Redis User RPC API 服务 客户端 Redis User RPC API 服务 客户端 #mermaid-svg-v6PgIQZQmG73FzaA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-v6PgIQZQmG73FzaA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-v6PgIQZQmG73FzaA .error-icon{fill:#552222;}#mermaid-svg-v6PgIQZQmG73FzaA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-v6PgIQZQmG73FzaA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-v6PgIQZQmG73FzaA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-v6PgIQZQmG73FzaA .marker.cross{stroke:#333333;}#mermaid-svg-v6PgIQZQmG73FzaA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-v6PgIQZQmG73FzaA p{margin:0;}#mermaid-svg-v6PgIQZQmG73FzaA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v6PgIQZQmG73FzaA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-v6PgIQZQmG73FzaA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-v6PgIQZQmG73FzaA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-v6PgIQZQmG73FzaA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-v6PgIQZQmG73FzaA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-v6PgIQZQmG73FzaA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-v6PgIQZQmG73FzaA .sequenceNumber{fill:white;}#mermaid-svg-v6PgIQZQmG73FzaA #sequencenumber{fill:#333;}#mermaid-svg-v6PgIQZQmG73FzaA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-v6PgIQZQmG73FzaA .messageText{fill:#333;stroke:none;}#mermaid-svg-v6PgIQZQmG73FzaA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v6PgIQZQmG73FzaA .labelText,#mermaid-svg-v6PgIQZQmG73FzaA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-v6PgIQZQmG73FzaA .loopText,#mermaid-svg-v6PgIQZQmG73FzaA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-v6PgIQZQmG73FzaA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-v6PgIQZQmG73FzaA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-v6PgIQZQmG73FzaA .noteText,#mermaid-svg-v6PgIQZQmG73FzaA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-v6PgIQZQmG73FzaA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v6PgIQZQmG73FzaA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v6PgIQZQmG73FzaA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-v6PgIQZQmG73FzaA .actorPopupMenu{position:absolute;}#mermaid-svg-v6PgIQZQmG73FzaA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-v6PgIQZQmG73FzaA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-v6PgIQZQmG73FzaA .actor-man circle,#mermaid-svg-v6PgIQZQmG73FzaA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-v6PgIQZQmG73FzaA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 登录请求登录验证登录成功,返回用户IDHSET user:online:status {userId} 1登录成功退出请求HDEL user:online:status {userId}退出成功

好友在线状态查询流程

用户登录后,API 服务需要返回所有好友的在线状态:

  1. 调用社交服务获取好友ID列表。
  2. 批量查询 Redisuser:online:status 的对应字段。
  3. 组装结果返回。

Redis Social RPC API 服务 客户端 Redis Social RPC API 服务 客户端 #mermaid-svg-tiZJxzsMeYjLnddG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tiZJxzsMeYjLnddG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tiZJxzsMeYjLnddG .error-icon{fill:#552222;}#mermaid-svg-tiZJxzsMeYjLnddG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tiZJxzsMeYjLnddG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tiZJxzsMeYjLnddG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tiZJxzsMeYjLnddG .marker.cross{stroke:#333333;}#mermaid-svg-tiZJxzsMeYjLnddG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tiZJxzsMeYjLnddG p{margin:0;}#mermaid-svg-tiZJxzsMeYjLnddG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tiZJxzsMeYjLnddG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-tiZJxzsMeYjLnddG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tiZJxzsMeYjLnddG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-tiZJxzsMeYjLnddG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-tiZJxzsMeYjLnddG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-tiZJxzsMeYjLnddG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-tiZJxzsMeYjLnddG .sequenceNumber{fill:white;}#mermaid-svg-tiZJxzsMeYjLnddG #sequencenumber{fill:#333;}#mermaid-svg-tiZJxzsMeYjLnddG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-tiZJxzsMeYjLnddG .messageText{fill:#333;stroke:none;}#mermaid-svg-tiZJxzsMeYjLnddG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tiZJxzsMeYjLnddG .labelText,#mermaid-svg-tiZJxzsMeYjLnddG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-tiZJxzsMeYjLnddG .loopText,#mermaid-svg-tiZJxzsMeYjLnddG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-tiZJxzsMeYjLnddG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-tiZJxzsMeYjLnddG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-tiZJxzsMeYjLnddG .noteText,#mermaid-svg-tiZJxzsMeYjLnddG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-tiZJxzsMeYjLnddG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tiZJxzsMeYjLnddG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tiZJxzsMeYjLnddG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-tiZJxzsMeYjLnddG .actorPopupMenu{position:absolute;}#mermaid-svg-tiZJxzsMeYjLnddG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-tiZJxzsMeYjLnddG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-tiZJxzsMeYjLnddG .actor-man circle,#mermaid-svg-tiZJxzsMeYjLnddG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-tiZJxzsMeYjLnddG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查询好友在线状态获取好友列表 (userId)好友ID列表HMGET user:online:status id1 id2 ...在线状态列表好友在线状态 Map

群成员在线状态查询流程

与好友类似,但查询的是指定群的成员ID,同样通过社交服务获取成员列表,然后批量查 Redis。客户端在打开群会话时触发请求,可配合定时器按需刷新。

代码实现

项目结构

假设项目名为 im,包含 api(网关)、rpc(用户服务、社交服务)等模块。与在线状态相关的改动主要集中在 api 层的业务逻辑和配置文件。

dir 复制代码
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 配置:

yaml 复制代码
Name: im-api 
Host: 0.0.0.0 
Port: 8888 
Redis:
  Host: 127.0.0.1:6379 
  Pass: ""
  Type: node 
UserRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379 
    Key: user.rpc 
SocialRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379 
    Key: social.rpc 

internal/config/config.go 中定义对应的配置结构体:

go 复制代码
package config 
 
import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
)
 
type Config struct {
	rest.RestConf 
	Redis    redis.RedisConf 
	UserRpc  zrpc.RpcClientConf 
	SocialRpc zrpc.RpcClientConf 
}

internal/svc/servicecontext.go 中初始化 Redis 和 RPC 客户端:

go 复制代码
package svc 
 
import (
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/zrpc"
	"im/api/internal/config"
	"im/rpc/user/userclient"
	"im/rpc/social/socialclient"
)
 
type ServiceContext struct {
	Config       config.Config 
	Redis        *redis.Redis 
	UserRpc      userclient.User 
	SocialRpc    socialclient.Social 
}
 
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		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 在线状态 Key:

go 复制代码
package consts 
 
const UserOnlineStatusKey = "user:online:status"

登录成功后设置在线状态

假设登录逻辑在 internal/logic/loginlogic.go,修改登录成功后的处理:

go 复制代码
package logic 
 
import (
	"context"
	"fmt"
	"im/api/internal/consts"
	"im/api/internal/svc"
	"im/api/internal/types"
	"im/rpc/user/userclient"
 
	"github.com/zeromicro/go-zero/core/logx"
)
 
type LoginLogic struct {
	logx.Logger 
	ctx    context.Context 
	svcCtx *svc.ServiceContext 
}
 
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
	return &LoginLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}
 
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
	// 调用用户 RPC 进行登录验证 
	loginResp, err := l.svcCtx.UserRpc.Login(l.ctx, &userclient.LoginRequest{
		Username: req.Username,
		Password: req.Password,
	})
	if err != nil {
		return nil, err 
	}
 
	userId := loginResp.UserId 
 
	// 登录成功,将用户 ID 写入 Redis 在线状态哈希表 
	_, err = l.svcCtx.Redis.Hset(consts.UserOnlineStatusKey, fmt.Sprintf("%d", userId), "1")
	if err != nil {
		l.Errorf("set user online status failed: %v", err)
		// 写入失败可根据业务决定是否让登录失败,这里仅记录日志并允许登录继续 
	}
 
	return &types.LoginResp{
		UserId: userId,
		Token:  loginResp.Token,
	}, nil 
}
'''
 
**退出登录**的处理类似,调用 `HDEL` 移除字段即可:
 
'''go 
func (l *LogoutLogic) Logout(req *types.LogoutReq) error {
	_, err := l.svcCtx.Redis.Hdel(consts.UserOnlineStatusKey, fmt.Sprintf("%d", req.UserId))
	return err 
}

好友在线状态查询接口

im.api 文件中定义请求/响应类型和接口:

go 复制代码
// 好友在线状态查询(无需请求参数,从 token 中获取当前用户 ID)
@server(
	jwt: Auth 
)
get /friends/online (FriendsOnlineReq) returns (FriendsOnlineResp)

对应 types/types.go

go 复制代码
type FriendsOnlineReq struct {
}
 
type FriendsOnlineResp struct {
	OnlineStatus map[int64]bool `json:"onlineStatus"` // key: 好友 ID, value: 是否在线 
}

internal/logic/friendsonlinelogic.go 中实现:

go 复制代码
package logic 
 
import (
	"context"
	"fmt"
	"im/api/internal/consts"
	"im/api/internal/svc"
	"im/api/internal/types"
	"im/rpc/social/socialclient"
 
	"github.com/zeromicro/go-zero/core/logx"
)
 
type FriendsOnlineLogic struct {
	logx.Logger 
	ctx    context.Context 
	svcCtx *svc.ServiceContext 
}
 
func NewFriendsOnlineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FriendsOnlineLogic {
	return &FriendsOnlineLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}
 
func (l *FriendsOnlineLogic) FriendsOnline(req *types.FriendsOnlineReq) (resp *types.FriendsOnlineResp, err error) {
	// 从上下文中获取当前登录用户 ID(由 JWT 中间件注入)
	userId := l.ctx.Value("userId").(int64)
 
	// 1. 调用社交服务获取好友 ID 列表 
	friendsResp, err := l.svcCtx.SocialRpc.GetFriendList(l.ctx, &socialclient.GetFriendListReq{
		UserId: userId,
	})
	if err != nil {
		return nil, err 
	}
 
	friendIds := friendsResp.FriendIds 
	if len(friendIds) == 0 {
		return &types.FriendsOnlineResp{OnlineStatus: map[int64]bool{}}, nil 
	}
 
	// 2. 构建 Redis HMGET 所需的字段列表 
	fields := make([]string, len(friendIds))
	for i, id := range friendIds {
		fields[i] = fmt.Sprintf("%d", id)
	}
 
	// 3. 批量查询在线状态 
	vals, err := l.svcCtx.Redis.Hmget(consts.UserOnlineStatusKey, fields...)
	if err != nil {
		return nil, err 
	}
 
	// 4. 组装结果 
	onlineStatus := make(map[int64]bool, len(friendIds))
	for i, id := range friendIds {
		onlineStatus[id] = vals[i] != ""
	}
 
	return &types.FriendsOnlineResp{OnlineStatus: onlineStatus}, nil 
}

群成员在线状态查询接口

与好友在线状态类似,但需要传入群 ID,并调用社交服务获取群成员列表。定义 API 和类型:

go 复制代码
// im.api 中增加 
@server(
	jwt: Auth 
)
post /group/members/online (GroupMembersOnlineReq) returns (GroupMembersOnlineResp)
go 复制代码
// types.go 
type GroupMembersOnlineReq struct {
	GroupId int64 `json:"groupId"`
}
 
type GroupMembersOnlineResp struct {
	OnlineStatus map[int64]bool `json:"onlineStatus"`
}

Logic 实现:

go 复制代码
package logic 
 
import (
	"context"
	"fmt"
	"im/api/internal/consts"
	"im/api/internal/svc"
	"im/api/internal/types"
	"im/rpc/social/socialclient"
)
 
type GroupMembersOnlineLogic struct {
	logx.Logger 
	ctx    context.Context 
	svcCtx *svc.ServiceContext 
}
 
func NewGroupMembersOnlineLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GroupMembersOnlineLogic {
	return &GroupMembersOnlineLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}
 
func (l *GroupMembersOnlineLogic) GroupMembersOnline(req *types.GroupMembersOnlineReq) (resp *types.GroupMembersOnlineResp, err error) {
	// 调用社交服务获取群成员 ID 列表 
	membersResp, err := l.svcCtx.SocialRpc.GetGroupMembers(l.ctx, &socialclient.GetGroupMembersReq{
		GroupId: req.GroupId,
	})
	if err != nil {
		return nil, err 
	}
 
	memberIds := membersResp.MemberIds 
	if len(memberIds) == 0 {
		return &types.GroupMembersOnlineResp{OnlineStatus: map[int64]bool{}}, nil 
	}
 
	// 构建字段列表,HMGET 查询 
	fields := make([]string, len(memberIds))
	for i, id := range memberIds {
		fields[i] = fmt.Sprintf("%d", id)
	}
 
	vals, err := l.svcCtx.Redis.Hmget(consts.UserOnlineStatusKey, fields...)
	if err != nil {
		return nil, err 
	}
 
	onlineStatus := make(map[int64]bool, len(memberIds))
	for i, id := range memberIds {
		onlineStatus[id] = vals[i] != ""
	}
 
	return &types.GroupMembersOnlineResp{OnlineStatus: onlineStatus}, nil 
}

测试与验证

启动所有服务后,使用 curl 或 Postman 进行测试。

  1. 登录用户 A,返回 userId: 1001
  2. 调用 GET /friends/online,查看好友在线状态(此时可能全部离线)。
  3. 使用另一个账号 B(userId: 1002)登录,再次调用 A 的好友在线状态接口,可看到 B 变为在线。
  4. 退出 B,再次查询,B 变为离线。
  5. 使用群成员在线接口 POST /group/members/online,传入 groupId,获得该群成员的在线情况。

测试过程中需确保 Redis 正常运行,且 RPC 服务返回正确的数据。

总结

利用 Go-Zero 微服务框架和 Redis Hash 结构,我们可以高效地实现 IM 系统的用户在线状态维护。该方案的优势在于:

  • 低延迟 :全部操作基于内存缓存,查询批量在线状态只需一次 HMGET
  • 架构清晰:状态存储与业务逻辑解耦,API 层通过 RPC 获取关系链数据,再与缓存交互。
  • 易扩展Hash 的字段值可以扩展为 JSON,记录设备类型、登录时间等丰富信息。

对于状态变更的实时性要求,可进一步引入 WebSocketServer-Sent Events 进行推送;在群聊场景中结合按需拉取,能够很好地平衡性能与用户体验。

相关推荐
找磁材1 小时前
空心杯电机与永磁体
开发语言·磁性材料·钕铁硼·磁钢
Patrick_Wilson1 小时前
为什么让程序主动崩溃反而更可靠?聊聊 supervisor 与 let it crash
后端·erlang·elixir
我不管我就要叫小猪2 小时前
C/C++----命名空间
c语言·开发语言·c++
自由随风飘2 小时前
JAVA中的面向对象-3
java·开发语言
小羊先生car2 小时前
RTOS-F429-HAL-(动/静态)任务的创建(2026/7/27)
开发语言·算法·c#
武子康3 小时前
权重已到、Recipe 还在变:Kimi K3 发布后该怎样做工程验收
人工智能·后端·agent
LONGZETECH3 小时前
新能源汽车动力系统仿真硬核拆解:五系统分层架构的设计逻辑与技术权衡
c语言·开发语言·架构·汽车·汽车仿真教学软件·汽车教学软件
站大爷IP3 小时前
列表推导式一用就爽?大数据量下它把我服务器内存榨干了,原来生成器才是yyds
后端
Slice_cy3 小时前
Mint 自研框架设计与实现:从重复开发走向配置驱动(二)
前端·后端·架构