分布式在线状态管理的完整实现
概述
OpenIM的在线状态管理是一个精心设计的分布式系统,涵盖从单节点连接管理到跨服务状态同步的完整链路。该系统通过六个关键阶段实现了高效、可靠的用户在线状态管理,支撑着OpenIM的实时消息推送和用户体验。
系统架构特点
🔄 事件驱动架构
- msggateway采用事件循环处理连接生命周期
- 基于通道的异步消息传递机制
- 非阻塞的状态变更处理
⚡ 多级缓存策略
- 本地连接映射:实时维护用户-设备连接关系
- Redis全局缓存:跨节点的状态一致性存储
- Push服务全量缓存:快速在线状态查询
- msggateway LRU缓存:热点用户状态优化
🚀 批量处理优化
- 状态变更的智能合并与分片
- 定时续约机制防止状态过期
- 并发RPC调用提升处理吞吐量
📡 分布式协调
- Redis发布订阅实现状态广播
- 跨节点多端登录冲突处理
- 用户状态订阅与实时推送
六大核心阶段
- msggateway本地连接映射 - 单节点用户连接状态管理
- 事件变更同步机制 - 状态变更的聚合与批量处理
- User RPC服务处理 - 状态数据的持久化存储
- Push服务全量缓存 - 推送决策的性能优化
- 用户状态订阅管理 - 实时状态变更通知
- 全局状态消息订阅 - 分布式状态一致性保证
阶段一:msggateway中的本地用户连接映射
1.1 网关启动监听机制
核心启动流程
在msggateway服务启动时,WsServer.Run()
方法会启动一个核心的事件监听协程,负责处理所有的连接生命周期事件:
go
// ws_server.go - WebSocket服务器事件循环
func (ws *WsServer) Run(done chan error) error {
var (
client *Client
netErr error
shutdownDone = make(chan struct{}, 1)
)
// 启动事件处理协程 - 整个连接管理的核心循环
go func() {
for {
select {
case <-shutdownDone:
// 收到关闭信号,退出事件循环
return
case client = <-ws.registerChan:
// 处理客户端注册事件
// 这是用户建立WebSocket连接后的第一个关键步骤
ws.registerClient(client)
case client = <-ws.unregisterChan:
// 处理客户端注销事件
// 用户断开连接或被踢下线时触发
ws.unregisterClient(client)
case onlineInfo := <-ws.kickHandlerChan:
// 处理踢下线事件
// 多端登录冲突时的处理逻辑
ws.multiTerminalLoginChecker(onlineInfo.clientOK, onlineInfo.oldClients, onlineInfo.newClient)
}
}
}()
// ... HTTP服务器启动逻辑
}
设计亮点:
- 事件驱动架构:使用通道机制实现非阻塞的事件处理
- 生命周期管理:统一处理连接的注册、注销和冲突处理
- 并发安全:通过通道序列化处理,避免并发竞争
1.2 用户注册流程深度解析
registerClient - 新连接注册核心逻辑
go
// ws_server.go - 客户端注册逻辑
func (ws *WsServer) registerClient(client *Client) {
var (
userOK bool // 用户是否已存在
clientOK bool // 同平台是否有连接
oldClients []*Client // 同平台的旧连接
)
// 关键步骤1:检查用户在指定平台的连接状态
// 这一步决定了后续的处理逻辑分支
oldClients, userOK, clientOK = ws.clients.Get(client.UserID, client.PlatformID)
if !userOK {
// 分支1:新用户首次连接
// 这是最简单的情况,直接添加到用户映射中
ws.clients.Set(client.UserID, client)
log.ZDebug(client.ctx, "user not exist", "userID", client.UserID, "platformID", client.PlatformID)
// 更新全局统计指标
prommetrics.OnlineUserGauge.Add(1) // Prometheus监控指标
ws.onlineUserNum.Add(1) // 在线用户数
ws.onlineUserConnNum.Add(1) // 在线连接数
} else {
// 分支2:用户已存在,需要处理多端登录策略
ws.multiTerminalLoginChecker(clientOK, oldClients, client)
log.ZDebug(client.ctx, "user exist", "userID", client.UserID, "platformID", client.PlatformID)
if clientOK {
// 分支2.1:同平台重复登录(如网络重连)
ws.clients.Set(client.UserID, client)
log.ZDebug(client.ctx, "repeat login", "userID", client.UserID, "platformID",
client.PlatformID, "old remote addr", getRemoteAdders(oldClients))
ws.onlineUserConnNum.Add(1)
} else {
// 分支2.2:新平台连接(如手机端新增PC端)
ws.clients.Set(client.UserID, client)
ws.onlineUserConnNum.Add(1)
}
}
// 关键步骤2:跨节点状态同步
// 在非k8s环境下,需要通知其他msggateway节点
wg := sync.WaitGroup{}
if ws.msgGatewayConfig.Discovery.Enable != "k8s" {
wg.Add(1)
go func() {
defer wg.Done()
// 向集群中其他节点发送用户上线信息
_ = ws.sendUserOnlineInfoToOtherNode(client.ctx, client)
}()
}
wg.Wait()
log.ZDebug(client.ctx, "user online", "online user Num", ws.onlineUserNum.Load(),
"online user conn Num", ws.onlineUserConnNum.Load())
}
核心设计思想:
- 状态检查优先:先检查现有状态,再决定处理策略
- 统计数据同步:实时更新监控指标,便于运维监控
- 集群协调:主动同步状态到其他节点,保证分布式一致性
1.3 用户映射操作详解
UserMap.Set - 状态变更的起点
go
// user_map.go - 添加用户连接并触发状态变更
func (u *userMap) Set(userID string, client *Client) {
u.lock.Lock()
defer u.lock.Unlock()
result, ok := u.data[userID]
if ok {
// 用户已存在,追加新连接
result.Clients = append(result.Clients, client)
} else {
// 新用户,创建平台连接信息
result = &UserPlatform{
Clients: []*Client{client},
}
u.data[userID] = result
}
// 关键操作:发送状态变更通知
// 这是连接状态同步的起点
u.push(client.UserID, result, nil)
}
push方法 - 状态变更事件发布
go
// user_map.go - 推送用户状态变更事件
func (u *userMap) push(userID string, userPlatform *UserPlatform, offline []int32) bool {
select {
case u.ch <- UserState{
UserID: userID,
Online: userPlatform.PlatformIDs(), // 当前在线的平台ID列表
Offline: offline, // 当前离线的平台ID列表
}:
// 成功发送状态变更事件
userPlatform.Time = time.Now() // 更新时间戳,用于续约判断
return true
default:
// 通道已满,丢弃事件避免阻塞
// 这是一个重要的容错机制
return false
}
}
设计亮点:
- 非阻塞设计:使用default分支避免通道阻塞
- 时间戳更新:为后续的续约机制提供时间基准
- 事件结构化:UserState包含完整的状态变更信息
1.4 用户注销流程
unregisterClient - 连接断开处理
go
// ws_server.go - 客户端注销逻辑
func (ws *WsServer) unregisterClient(client *Client) {
// 关键步骤1:对象池回收(性能优化)
defer ws.clientPool.Put(client)
// 关键步骤2:从用户映射中删除连接
isDeleteUser := ws.clients.DeleteClients(client.UserID, []*Client{client})
// 关键步骤3:更新统计数据
if isDeleteUser {
// 用户完全离线
ws.onlineUserNum.Add(-1)
prommetrics.OnlineUserGauge.Dec()
}
ws.onlineUserConnNum.Add(-1)
// 关键步骤4:清理订阅关系
ws.subscription.DelClient(client)
log.ZDebug(client.ctx, "user offline", "close reason", client.closedErr,
"online user Num", ws.onlineUserNum.Load(),
"online user conn Num", ws.onlineUserConnNum.Load())
}
DeleteClients - 精确连接移除
go
// user_map.go - 删除指定用户连接
func (u *userMap) DeleteClients(userID string, clients []*Client) (isDeleteUser bool) {
if len(clients) == 0 {
return false
}
u.lock.Lock()
defer u.lock.Unlock()
result, ok := u.data[userID]
if !ok {
return false
}
// 关键逻辑:记录要删除的平台ID
offline := make([]int32, 0, len(clients))
// 创建待删除连接的地址集合,用于快速查找
deleteAddr := datautil.SliceSetAny(clients, func(client *Client) string {
return client.ctx.GetRemoteAddr()
})
// 重新构建连接列表,排除要删除的连接
tmp := result.Clients
result.Clients = result.Clients[:0] // 重置切片但保留容量
for _, client := range tmp {
if _, delCli := deleteAddr[client.ctx.GetRemoteAddr()]; delCli {
// 记录离线的平台ID
offline = append(offline, int32(client.PlatformID))
} else {
// 保留未删除的连接
result.Clients = append(result.Clients, client)
}
}
// 关键操作:发送离线状态变更通知
defer u.push(userID, result, offline)
// 检查是否需要删除用户记录
if len(result.Clients) > 0 {
return false // 还有剩余连接
}
// 删除用户记录,释放内存
delete(u.data, userID)
return true
}
核心设计思想:
- 精确删除:基于连接地址进行精确匹配删除
- 状态完整性:同时记录在线和离线状态变更
- 内存管理:及时清理无效连接,避免内存泄漏
- 事件触发:删除操作同样会触发状态变更通知
阶段二:msggateway中的事件变更同步
2.1 状态同步协程启动
ChangeOnlineStatus - 核心状态处理器
go
// online.go - 用户在线状态变更处理器核心入口
func (ws *WsServer) ChangeOnlineStatus(concurrent int) {
// 并发数最小为1,确保至少有一个处理协程
if concurrent < 1 {
concurrent = 1
}
// 续约时间设置为缓存过期时间的1/3,确保及时续约
// 这样设计可以在缓存过期前有足够的时间进行多次续约尝试
const renewalTime = cachekey.OnlineExpire / 3
renewalTicker := time.NewTicker(renewalTime)
// 为每个并发处理器创建独立的请求通道,避免竞争
// 通道缓冲大小为64,平衡内存使用和处理延迟
requestChs := make([]chan *pbuser.SetUserOnlineStatusReq, concurrent)
// 为每个处理器创建状态变更缓冲区,用于批量合并
changeStatus := make([][]UserState, concurrent)
// 初始化每个并发处理器的通道和缓冲区
for i := 0; i < concurrent; i++ {
requestChs[i] = make(chan *pbuser.SetUserOnlineStatusReq, 64)
changeStatus[i] = make([]UserState, 0, 100) // 预分配容量,减少内存重分配
}
// 合并定时器:每秒钟强制推送一次积累的状态变更
// 确保即使未达到批次大小,状态也能及时同步
mergeTicker := time.NewTicker(time.Second)
// ... 后续逻辑
}
设计核心思想:
- 并发处理:通过多个协程并行处理状态变更,提升吞吐量
- 哈希分片:确保同一用户的操作顺序性
- 批量缓冲:减少RPC调用次数,提升性能
2.2 续约机制深度解析
为什么需要续约机制?
go
// Redis TTL机制的挑战:
// 1. Redis中的用户在线状态设置了30分钟的TTL
// 2. 如果服务突然宕机,用户状态会一直保持"在线"直到TTL过期
// 3. 这期间离线用户可能收不到离线推送通知
// 4. 续约机制确保长连接用户的状态不会意外过期
// 续约间隔计算
const renewalTime = cachekey.OnlineExpire / 3 // 10分钟续约一次
续约处理逻辑
go
// online.go - 续约事件处理
case now := <-renewalTicker.C:
// 定时续约:定期续约在线用户状态,防止缓存过期
// 设计思路:
// - 计算续约截止时间:当前时间减去续约间隔
// - 获取需要续约的用户列表
// - 批量更新这些用户的在线状态
deadline := now.Add(-cachekey.OnlineExpire / 3)
users := ws.clients.GetAllUserStatus(deadline, now)
log.ZDebug(context.Background(), "renewal ticker", "deadline", deadline,
"nowtime", now, "num", len(users), "users", users)
pushUserState(users...)
GetAllUserStatus - 批量状态获取
go
// user_map.go - 获取需要续约的用户状态
func (u *userMap) GetAllUserStatus(deadline time.Time, nowtime time.Time) (result []UserState) {
u.lock.RLock()
defer u.lock.RUnlock()
result = make([]UserState, 0, len(u.data))
for userID, userPlatform := range u.data {
// 跳过时间戳晚于截止时间的记录(最近已更新过的)
if deadline.Before(userPlatform.Time) {
continue
}
// 更新时间戳,标记为已处理
userPlatform.Time = nowtime
// 构建在线平台列表
online := make([]int32, 0, len(userPlatform.Clients))
for _, client := range userPlatform.Clients {
online = append(online, int32(client.PlatformID))
}
// 添加到结果列表
result = append(result, UserState{
UserID: userID,
Online: online,
})
}
return result
}
续约机制设计亮点:
- 时间窗口控制:只对超过时间阈值的用户进行续约
- 批量处理:一次处理多个用户的续约请求
- 时间戳更新:避免重复续约同一用户
2.3 批量合并策略
pushUserState - 智能分片与缓冲
go
// online.go - 推送用户状态变更到对应的处理器
pushUserState := func(us ...UserState) {
for _, u := range us {
// 计算用户ID的MD5哈希,用于分片
sum := md5.Sum([]byte(u.UserID))
// 结合随机数和哈希值,计算分片索引
i := (binary.BigEndian.Uint64(sum[:]) + rNum) % uint64(concurrent)
// 添加到对应分片的缓冲区
changeStatus[i] = append(changeStatus[i], u)
status := changeStatus[i]
// 当缓冲区达到容量上限时,立即发送批量请求
if len(status) == cap(status) {
req := &pbuser.SetUserOnlineStatusReq{
Status: datautil.Slice(status, local2pb),
}
changeStatus[i] = status[:0] // 重置缓冲区,复用底层数组
select {
case requestChs[i] <- req:
// 成功发送到处理通道
default:
// 处理通道已满,记录警告日志
log.ZError(context.Background(), "user online processing is too slow", nil)
}
}
}
}
pushAllUserState - 定时强制刷新
go
// online.go - 强制推送所有缓冲区中的状态变更
pushAllUserState := func() {
for i, status := range changeStatus {
if len(status) == 0 {
continue // 跳过空缓冲区
}
req := &pbuser.SetUserOnlineStatusReq{
Status: datautil.Slice(status, local2pb),
}
changeStatus[i] = status[:0] // 重置缓冲区
select {
case requestChs[i] <- req:
// 成功发送
default:
// 通道阻塞,记录警告
log.ZError(context.Background(), "user online processing is too slow", nil)
}
}
}
2.4 三种触发机制
主事件循环
go
// online.go - 主事件循环:处理三种类型的事件
for {
select {
case <-mergeTicker.C:
// 触发1:定时合并推送(每秒一次)
// 确保状态更新的实时性,避免因批次未满而延迟
pushAllUserState()
case now := <-renewalTicker.C:
// 触发2:定时续约(每10分钟一次)
// 防止Redis缓存过期导致的状态丢失
deadline := now.Add(-cachekey.OnlineExpire / 3)
users := ws.clients.GetAllUserStatus(deadline, now)
log.ZDebug(context.Background(), "renewal ticker", "deadline", deadline,
"nowtime", now, "num", len(users), "users", users)
pushUserState(users...)
case state := <-ws.clients.UserState():
// 触发3:实时状态变更(即时响应)
// 处理来自客户端连接管理器的状态变更事件
log.ZDebug(context.Background(), "OnlineCache user online change",
"userID", state.UserID, "online", state.Online, "offline", state.Offline)
pushUserState(state)
}
}
三种触发机制的设计思想:
- 实时响应:用户连接变化立即处理
- 定时保证:每秒强制刷新确保不丢失
- 续约保活:定期续约防止缓存过期
2.5 并发RPC调用
doRequest - 执行状态更新
go
// online.go - 执行具体的状态更新请求
doRequest := func(req *pbuser.SetUserOnlineStatusReq) {
// 生成唯一操作ID,便于日志追踪和问题排查
opIdCtx := mcontext.SetOperationID(context.Background(),
operationIDPrefix+strconv.FormatInt(count.Add(1), 10))
// 设置5秒超时,避免长时间阻塞
ctx, cancel := context.WithTimeout(opIdCtx, time.Second*5)
defer cancel()
// 调用用户服务更新在线状态
if err := ws.userClient.SetUserOnlineStatus(ctx, req); err != nil {
log.ZError(ctx, "update user online status", err)
}
// 处理状态变更的 webhook 回调
for _, ss := range req.Status {
// 处理上线事件的 webhook
for _, online := range ss.Online {
// 获取客户端连接信息,判断是否为后台模式
client, _, _ := ws.clients.Get(ss.UserID, int(online))
back := false
if len(client) > 0 {
back = client[0].IsBackground
}
// 触发用户上线 webhook
ws.webhookAfterUserOnline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOnline,
ss.UserID, int(online), back, ss.ConnID)
}
// 处理下线事件的 webhook
for _, offline := range ss.Offline {
ws.webhookAfterUserOffline(ctx, &ws.msgGatewayConfig.WebhooksConfig.AfterUserOffline,
ss.UserID, int(offline), ss.ConnID)
}
}
}
并发处理器启动
go
// online.go - 启动并发处理协程
for i := 0; i < concurrent; i++ {
go func(ch <-chan *pbuser.SetUserOnlineStatusReq) {
// 持续处理通道中的请求,直到通道关闭
for req := range ch {
doRequest(req)
}
}(requestChs[i])
}
并发设计亮点:
- 操作ID追踪:每个请求都有唯一ID,便于问题排查
- 超时控制:5秒超时避免长时间阻塞
- Webhook集成:状态变更自动触发外部回调
阶段三:User RPC服务处理
3.1 User服务初始化
Redis在线缓存初始化
go
// user/user.go - 用户服务启动初始化
func Start(ctx context.Context, config *Config, client registry.SvcDiscoveryRegistry, server *grpc.Server) error {
// 初始化Redis连接
rdb, err := redisutil.NewRedisClient(ctx, config.RedisConfig.Build())
if err != nil {
return err
}
// 创建用户服务实例
u := &userServer{
online: redis.NewUserOnline(rdb), // 关键:初始化Redis在线状态缓存
db: database,
// ... 其他组件初始化
}
// 注册用户服务到gRPC服务器
pbuser.RegisterUserServer(server, u)
return nil
}
初始化关键点:
redis.NewUserOnline(rdb)
创建了Redis在线状态缓存实例- 这个实例实现了
cache.OnlineCache
接口 - 为后续的状态操作提供了Redis访问能力
3.2 状态更新请求处理
SetUserOnlineStatus - 批量状态更新入口
go
// user/online.go - 批量设置多个用户的在线状态
func (s *userServer) SetUserOnlineStatus(ctx context.Context, req *pbuser.SetUserOnlineStatusReq) (*pbuser.SetUserOnlineStatusResp, error) {
// 遍历每个用户的状态信息进行更新
for _, status := range req.Status {
// 调用底层Redis操作接口
if err := s.online.SetUserOnline(ctx, status.UserID, status.Online, status.Offline); err != nil {
return nil, err
}
}
return &pbuser.SetUserOnlineStatusResp{}, nil
}
处理特点:
- 批量处理:一次请求可以更新多个用户状态
- 循环调用:逐个调用底层Redis操作
- 错误传播:任何一个操作失败都会返回错误
3.3 Redis状态操作详解
SetUserOnline - 核心状态更新逻辑
go
// redis/online.go - 原子性设置用户在线状态
func (s *userOnline) SetUserOnline(ctx context.Context, userID string, online, offline []int32) error {
// Lua脚本:原子性执行用户在线状态更新
// 脚本参数说明:
// KEYS[1]: 用户在线状态键
// ARGV[1]: 键过期时间(秒)
// ARGV[2]: 当前时间戳(用于清理过期数据)
// ARGV[3]: 未来过期时间戳(作为新在线状态的score)
// ARGV[4]: 离线平台数量
// ARGV[5...]: 离线平台ID列表 + 在线平台ID列表
script := `
local key = KEYS[1]
local score = ARGV[3]
-- 记录操作前的成员数量
local num1 = redis.call("ZCARD", key)
-- 清理过期的成员(score小于当前时间戳)
redis.call("ZREMRANGEBYSCORE", key, "-inf", ARGV[2])
-- 移除指定的离线平台
for i = 5, tonumber(ARGV[4])+4 do
redis.call("ZREM", key, ARGV[i])
end
-- 记录移除操作后的成员数量
local num2 = redis.call("ZCARD", key)
-- 添加新的在线平台,score为未来过期时间戳
for i = 5+tonumber(ARGV[4]), #ARGV do
redis.call("ZADD", key, score, ARGV[i])
end
-- 设置键的过期时间,防止内存泄漏
redis.call("EXPIRE", key, ARGV[1])
-- 记录最终的成员数量
local num3 = redis.call("ZCARD", key)
-- 判断是否发生了实际变更
local change = (num1 ~= num2) or (num2 ~= num3)
if change then
-- 状态发生变更,返回当前所有在线平台 + 变更标志
local members = redis.call("ZRANGE", key, 0, -1)
table.insert(members, "1") -- 添加变更标志
return members
else
-- 状态未变更,返回无变更标志
return {"0"}
end
`
// 构建脚本参数
now := time.Now()
argv := make([]any, 0, 2+len(online)+len(offline))
argv = append(argv,
int32(s.expire/time.Second), // 键过期时间(秒)
now.Unix(), // 当前时间戳
now.Add(s.expire).Unix(), // 未来过期时间戳
int32(len(offline)), // 离线平台数量
)
// 添加离线平台ID列表
for _, platformID := range offline {
argv = append(argv, platformID)
}
// 添加在线平台ID列表
for _, platformID := range online {
argv = append(argv, platformID)
}
// 执行Lua脚本
keys := []string{s.getUserOnlineKey(userID)}
platformIDs, err := s.rdb.Eval(ctx, script, keys, argv).StringSlice()
if err != nil {
log.ZError(ctx, "redis SetUserOnline", err, "userID", userID, "online", online, "offline", offline)
return err
}
// 检查返回值有效性
if len(platformIDs) == 0 {
return errs.ErrInternalServer.WrapMsg("SetUserOnline redis lua invalid return value")
}
// 检查是否需要发布状态变更通知
if platformIDs[len(platformIDs)-1] != "0" {
// 状态发生了变更,发布通知消息
log.ZDebug(ctx, "redis SetUserOnline push", "userID", userID, "online", online, "offline", offline, "platformIDs", platformIDs[:len(platformIDs)-1])
// 构建通知消息:平台ID列表 + 用户ID,用冒号分隔
platformIDs[len(platformIDs)-1] = userID // 替换变更标志为用户ID
msg := strings.Join(platformIDs, ":")
// 发布到状态变更通知频道
if err := s.rdb.Publish(ctx, s.channelName, msg).Err(); err != nil {
return errs.Wrap(err)
}
} else {
// 状态未发生变更,记录调试日志
log.ZDebug(ctx, "redis SetUserOnline not push", "userID", userID, "online", online, "offline", offline)
}
return nil
}
阶段四:Push服务本地维护全量在线状态
4.1 Push服务启动初始化
Push服务在启动时建立了完整的在线状态本地缓存体系,通过全量加载和增量订阅相结合的方式,确保状态数据的完整性和实时性。
go
// push/push_handler.go - ConsumerHandler创建过程
func NewConsumerHandler(ctx context.Context, config *Config, database controller.PushDatabase,
offlinePusher offlinepush.OfflinePusher, rdb redis.UniversalClient,
client discovery.SvcDiscoveryRegistry) (*ConsumerHandler, error) {
// 创建OnlineCache实例,使用全量缓存模式
// fullUserCache=true: 缓存所有用户的在线状态
// 设计思路:推送服务需要快速判断用户是否在线,全量缓存提供最佳性能
consumerHandler.onlineCache, err = rpccache.NewOnlineCache(
consumerHandler.userClient, // 用户服务RPC客户端
consumerHandler.groupLocalCache, // 群组本地缓存
rdb, // Redis客户端
config.RpcConfig.FullUserCache, // true - 启用全量缓存
nil) // 无状态变更回调
if err != nil {
return nil, err
}
return &consumerHandler, nil
}
4.2 全量缓存模式核心数据结构
go
// pkg/rpccache/online.go - OnlineCache核心结构
type OnlineCache struct {
client *rpcli.UserClient // 用户服务RPC客户端
group *GroupLocalCache // 群组本地缓存引用
// 缓存策略标志位
// fullUserCache=true: 使用mapCache缓存所有用户状态
// fullUserCache=false: 使用lruCache只缓存热点用户
fullUserCache bool
// 两种缓存实现策略
lruCache lru.LRU[string, []int32] // LRU缓存(热点数据)
mapCache *cacheutil.Cache[string, []int32] // 全量缓存(所有用户)
// 三阶段初始化控制
Lock *sync.Mutex // 保护条件变量的互斥锁
Cond *sync.Cond // 用于阶段同步的条件变量
CurrentPhase atomic.Uint32 // 当前初始化阶段标识
}
// 三阶段初始化常量
const (
Begin uint32 = iota // 开始阶段
DoOnlineStatusOver // 在线状态初始化完成
DoSubscribeOver // 订阅初始化完成
)
4.3 全量状态初始化过程
4.3.1 initUsersOnlineStatus核心实现
go
// pkg/rpccache/online.go - 全量初始化在线状态
func (o *OnlineCache) initUsersOnlineStatus(ctx context.Context) (err error) {
log.ZDebug(ctx, "init users online status begin")
var (
totalSet atomic.Int64 // 原子计数器,统计处理的用户总数
maxTries = 5 // 最大重试次数,提高网络调用可靠性
retryInterval = time.Second * 5 // 重试间隔
resp *user.GetAllOnlineUsersResp // RPC响应对象
)
// 确保阶段切换和性能统计
defer func(t time.Time) {
log.ZInfo(ctx, "init users online status end",
"cost", time.Since(t), "totalSet", totalSet.Load())
// 切换到下一阶段,通知等待的协程
o.CurrentPhase.Store(DoOnlineStatusOver)
o.Cond.Broadcast()
}(time.Now())
// 重试机制封装,提高网络调用的可靠性
retryOperation := func(operation func() error, operationName string) error {
for i := 0; i < maxTries; i++ {
if err = operation(); err != nil {
log.ZWarn(ctx, fmt.Sprintf("initUsersOnlineStatus: %s failed", operationName), err)
time.Sleep(retryInterval)
} else {
return nil
}
}
return err
}
// 分页获取所有在线用户,cursor为分页游标
cursor := uint64(0)
for resp == nil || resp.NextCursor != 0 {
if err = retryOperation(func() error {
// 调用用户服务RPC获取一页在线用户数据
resp, err = o.client.GetAllOnlineUsers(ctx, cursor)
if err != nil {
return err
}
// 处理当前页的用户状态
for _, u := range resp.StatusList {
if u.Status == constant.Online {
// 只缓存在线用户,减少内存占用
o.setUserOnline(u.UserID, u.PlatformIDs)
}
totalSet.Add(1) // 统计处理数量
}
cursor = resp.NextCursor // 更新游标到下一页
return nil
}, "getAllOnlineUsers"); err != nil {
return err
}
}
return nil
}
4.3.2 GetAllOnlineUsers底层实现分析
go
// pkg/common/storage/cache/redis/online.go - Redis层实现
func (s *userOnline) GetAllOnlineUsers(ctx context.Context, cursor uint64) (map[string][]int32, uint64, error) {
result := make(map[string][]int32)
// 使用SCAN命令分页扫描所有在线状态键
// 设计思路:避免KEYS命令阻塞,支持大数据集分页遍历
keys, nextCursor, err := s.rdb.Scan(ctx, cursor,
fmt.Sprintf("%s*", cachekey.OnlineKey), // 模式匹配在线状态键
constant.ParamMaxLength).Result() // 分页大小
if err != nil {
return nil, 0, err
}
// 遍历每个用户的在线状态键
for _, key := range keys {
// 从键中提取用户ID
userID := cachekey.GetOnlineKeyUserID(key)
// 获取该用户所有平台的在线状态(使用ZRANGE命令)
strValues, err := s.rdb.ZRange(ctx, key, 0, -1).Result()
if err != nil {
return nil, 0, err
}
// 转换平台ID格式
values := make([]int32, 0, len(strValues))
for _, value := range strValues {
intValue, err := strconv.Atoi(value)
if err != nil {
return nil, 0, errs.Wrap(err)
}
values = append(values, int32(intValue))
}
result[userID] = values
}
return result, nextCursor, nil
}
4.3.3 Redis SCAN方法的性能与一致性问题分析
一、性能与效率问题
-
遍历耗时过长
- 问题:当数据集超大(如百万级Key)时,即使分批扫描,多次网络IO和遍历操作仍显著增加总耗时
- 影响:可能拖慢客户端响应,导致初始化阶段过长
- 优化方案:调整COUNT参数(建议100~1000),平衡单次返回量与服务端负载
-
服务端负载压力
- 问题:COUNT值过大会令单次SCAN逼近KEYS的阻塞风险;过小则增加迭代次数
- 建议:根据数据集规模压测选择合理COUNT值,避免极端设置
二、结果集的不确定性
-
重复或遗漏数据
- 原因:SCAN基于游标增量遍历哈希桶,若遍历中触发Rehash(扩容/缩容),可能重复扫描部分Key或遗漏迁移中的Key
- 应对策略:客户端需处理重复Key(如用Set去重),缩容场景建议避免同时写入
-
非实时快照
- 问题:SCAN不保证返回遍历开始时的完整快照。若遍历期间Key过期或被删,可能缺失;新增Key可能被纳入
- 影响:适合统计等容忍偏差场景,不适合强一致性需求
4.4 初始化与订阅的并发协作机制
go
// pkg/rpccache/online.go - 订阅处理协程
func (o *OnlineCache) doSubscribe(ctx context.Context, rdb redis.UniversalClient,
fn func(ctx context.Context, userID string, platformIDs []int32)) {
o.Lock.Lock()
// 步骤1:立即订阅Redis频道,确保不漏掉任何状态变更
// 设计思路:先订阅再初始化,避免初始化期间的状态变更丢失
ch := rdb.Subscribe(ctx, cachekey.OnlineChannel).Channel()
// 步骤2:等待在线状态初始化完成
// 同步机制:确保全量数据加载完成后再处理增量消息
for o.CurrentPhase.Load() < DoOnlineStatusOver {
o.Cond.Wait() // 等待initUsersOnlineStatus完成
}
o.Lock.Unlock()
log.ZInfo(ctx, "begin doSubscribe")
// 步骤3:消息处理函数,根据缓存策略选择不同的处理逻辑
doMessage := func(message *redis.Message) {
// 解析Redis消息,提取用户ID和平台ID列表
userID, platformIDs, err := useronline.ParseUserOnlineStatus(message.Payload)
if err != nil {
log.ZError(ctx, "OnlineCache setHasUserOnline redis subscribe parseUserOnlineStatus",
err, "payload", message.Payload, "channel", message.Channel)
return
}
log.ZDebug(ctx, fmt.Sprintf("get subscribe %s message", cachekey.OnlineChannel),
"useID", userID, "platformIDs", platformIDs)
switch o.fullUserCache {
case true:
// 全量缓存模式:直接更新mapCache
if len(platformIDs) == 0 {
// 平台列表为空表示用户离线,删除缓存记录
o.mapCache.Delete(userID)
} else {
// 更新用户在线状态
o.mapCache.Store(userID, platformIDs)
}
case false:
// LRU缓存模式:更新lruCache并调用回调函数
storageCache := o.setHasUserOnline(userID, platformIDs)
log.ZDebug(ctx, "OnlineCache setHasUserOnline",
"userID", userID, "platformIDs", platformIDs,
"payload", message.Payload, "storageCache", storageCache)
if fn != nil {
fn(ctx, userID, platformIDs) // 执行外部回调
}
}
}
// 步骤4:处理初始化完成后的积压消息
// 设计思路:确保初始化期间的消息都被正确处理
if o.CurrentPhase.Load() == DoOnlineStatusOver {
for done := false; !done; {
select {
case message := <-ch:
doMessage(message) // 处理积压消息
default:
// 没有积压消息,切换到订阅完成阶段
o.CurrentPhase.Store(DoSubscribeOver)
o.Cond.Broadcast()
done = true
}
}
}
// 步骤5:进入正常的消息处理循环
for message := range ch {
doMessage(message)
}
}
阶段五:msggateway处理用户订阅其他用户在线状态事件
5.1 订阅管理器数据结构设计
go
// internal/msggateway/subscription.go - 订阅管理核心结构
type Subscription struct {
lock sync.RWMutex // 读写锁,保护并发访问
userIDs map[string]*subClient // 被订阅用户ID -> 订阅该用户的客户端集合
}
// subClient 订阅某个用户的客户端集合
// 设计思路:使用map存储客户端连接,key为连接地址,便于快速查找和删除
type subClient struct {
clients map[string]*Client // key: 客户端连接地址, value: 客户端连接对象
}
// 数据结构说明:
// userIDs["user123"] -> subClient{
// clients: {
// "192.168.1.100:8080": client1, // iOS客户端
// "192.168.1.101:8081": client2, // Android客户端
// "192.168.1.102:8082": client3, // Web客户端
// }
// }
5.2 客户端订阅状态管理
go
// internal/msggateway/client.go - 客户端订阅状态
type Client struct {
// ... 其他字段
subLock *sync.Mutex // 订阅操作互斥锁
subUserIDs map[string]struct{} // 客户端订阅的用户ID列表
// 数据结构示例:
// subUserIDs = {
// "user456": {}, // 订阅user456的状态
// "user789": {}, // 订阅user789的状态
// }
}
// ResetClient 重置客户端状态时初始化订阅结构
func (c *Client) ResetClient(ctx *UserConnContext, conn LongConn, longConnServer LongConnServer) {
// ... 其他初始化代码
c.subLock = new(sync.Mutex)
// 清理旧的订阅列表,避免内存泄漏
if c.subUserIDs != nil {
clear(c.subUserIDs)
}
c.subUserIDs = make(map[string]struct{})
}
5.3 订阅消息处理流程
go
// internal/msggateway/client.go - 客户端消息处理
func (c *Client) handleMessage(message []byte) error {
// ... 消息解压缩和反序列化逻辑
// 基于请求标识符的消息路由
switch binaryReq.ReqIdentifier {
case WsSubUserOnlineStatus:
// 处理用户在线状态订阅请求
// 设计思路:客户端主动订阅关心的用户状态变更
resp, messageErr = c.longConnServer.SubUserOnlineStatus(ctx, c, binaryReq)
// ... 其他消息类型处理
}
return c.replyMessage(ctx, binaryReq, messageErr, resp)
}
5.4 订阅逻辑核心实现
go
// internal/msggateway/subscription.go - 订阅处理核心逻辑
func (ws *WsServer) SubUserOnlineStatus(ctx context.Context, client *Client, data *Req) ([]byte, error) {
var sub sdkws.SubUserOnlineStatus
// 反序列化订阅请求
if err := proto.Unmarshal(data.Data, &sub); err != nil {
return nil, err
}
// 更新客户端的订阅关系
// 设计思路:支持批量订阅和取消订阅,提高操作效率
ws.subscription.Sub(client, sub.SubscribeUserID, sub.UnsubscribeUserID)
// 构建订阅响应,立即返回新订阅用户的当前状态
var resp sdkws.SubUserOnlineStatusTips
if len(sub.SubscribeUserID) > 0 {
resp.Subscribers = make([]*sdkws.SubUserOnlineStatusElem, 0, len(sub.SubscribeUserID))
// 为每个新订阅的用户获取当前在线状态
for _, userID := range sub.SubscribeUserID {
// 从阶段四的在线缓存中获取状态
platformIDs, err := ws.online.GetUserOnlinePlatform(ctx, userID)
if err != nil {
return nil, err
}
resp.Subscribers = append(resp.Subscribers, &sdkws.SubUserOnlineStatusElem{
UserID: userID,
OnlinePlatformIDs: platformIDs,
})
}
}
return proto.Marshal(&resp)
}
5.5 订阅关系管理详细实现
go
// internal/msggateway/subscription.go - 订阅关系管理
func (s *Subscription) Sub(client *Client, addUserIDs, delUserIDs []string) {
if len(addUserIDs)+len(delUserIDs) == 0 {
return // 没有订阅变更
}
var (
del = make(map[string]struct{}) // 要删除的订阅
add = make(map[string]struct{}) // 要添加的订阅
)
// 第一步:更新客户端的订阅列表
client.subLock.Lock()
// 处理取消订阅
for _, userID := range delUserIDs {
if _, ok := client.subUserIDs[userID]; !ok {
continue // 客户端未订阅该用户
}
del[userID] = struct{}{}
delete(client.subUserIDs, userID)
}
// 处理新增订阅
for _, userID := range addUserIDs {
delete(del, userID) // 优化:如果同时取消和订阅同一用户,则忽略取消操作
if _, ok := client.subUserIDs[userID]; ok {
continue // 客户端已订阅该用户
}
client.subUserIDs[userID] = struct{}{}
add[userID] = struct{}{}
}
client.subLock.Unlock()
if len(del)+len(add) == 0 {
return // 没有实际的订阅变更
}
// 第二步:更新全局订阅映射
addr := client.ctx.GetRemoteAddr()
s.lock.Lock()
defer s.lock.Unlock()
// 处理取消订阅
for userID := range del {
sub, ok := s.userIDs[userID]
if !ok {
continue
}
delete(sub.clients, addr)
// 如果该用户没有任何订阅者,删除该用户的映射
if len(sub.clients) == 0 {
delete(s.userIDs, userID)
}
}
// 处理新增订阅
for userID := range add {
sub, ok := s.userIDs[userID]
if !ok {
// 创建新的订阅客户端集合
sub = &subClient{clients: make(map[string]*Client)}
s.userIDs[userID] = sub
}
sub.clients[addr] = client
}
}
5.6 订阅关系清理机制
go
// internal/msggateway/subscription.go - 客户端断开时的清理
func (s *Subscription) DelClient(client *Client) {
// 获取客户端订阅的所有用户ID
client.subLock.Lock()
userIDs := datautil.Keys(client.subUserIDs)
for _, userID := range userIDs {
delete(client.subUserIDs, userID)
}
client.subLock.Unlock()
if len(userIDs) == 0 {
return // 客户端没有订阅任何用户
}
// 从全局订阅映射中移除该客户端
addr := client.ctx.GetRemoteAddr()
s.lock.Lock()
defer s.lock.Unlock()
for _, userID := range userIDs {
sub, ok := s.userIDs[userID]
if !ok {
continue
}
delete(sub.clients, addr)
// 如果该用户没有任何订阅者,删除该用户的映射
if len(sub.clients) == 0 {
delete(s.userIDs, userID)
}
}
}
// internal/msggateway/ws_server.go - 客户端注销时调用
func (ws *WsServer) unregisterClient(client *Client) {
// ... 其他清理逻辑
// 清理订阅关系,防止内存泄漏
ws.subscription.DelClient(client)
// ... 统计更新和日志记录
}
阶段六:msggateway订阅Redis的全局用户状态变更消息
6.1 msggateway的LRU缓存模式初始化
go
// internal/msggateway/init.go - msggateway启动配置
func Start(ctx context.Context, index int, conf *Config) error {
// ... 初始化代码
hubServer := NewServer(longServer, conf, func(srv *Server) error {
var err error
// msggateway使用LRU缓存模式,fullUserCache=false
// 设计思路:msggateway主要处理实时连接,使用LRU缓存热点用户即可
longServer.online, err = rpccache.NewOnlineCache(
srv.userClient, // 用户服务RPC客户端
nil, // 不依赖群组缓存
rdb, // Redis客户端
false, // fullUserCache=false,使用LRU模式
longServer.subscriberUserOnlineStatusChanges) // 状态变更回调函数
return err
})
// ... 其他启动逻辑
}
6.2 LRU缓存模式的核心逻辑
go
// pkg/rpccache/online.go - LRU模式初始化
func NewOnlineCache(client *rpcli.UserClient, group *GroupLocalCache, rdb redis.UniversalClient,
fullUserCache bool, fn func(ctx context.Context, userID string, platformIDs []int32)) (*OnlineCache, error) {
x := &OnlineCache{
client: client,
group: group,
fullUserCache: fullUserCache,
Lock: l,
Cond: sync.NewCond(l),
}
switch x.fullUserCache {
case false:
// LRU缓存模式:使用分片LRU缓存热点用户状态
log.ZDebug(ctx, "fullUserCache is false")
// 创建1024个分片的LRU缓存,每个分片容量2048
// 缓存有效期为OnlineExpire/2,清理间隔3秒
x.lruCache = lru.NewSlotLRU(1024, localcache.LRUStringHash, func() lru.LRU[string, []int32] {
return lru.NewLayLRU[string, []int32](
2048, // 单分片容量
cachekey.OnlineExpire/2, // 缓存有效期
time.Second*3, // 清理间隔
localcache.EmptyTarget{}, // 空目标处理
func(key string, value []int32) {}) // 淘汰回调
})
// LRU模式下直接进入订阅阶段,无需全量初始化
x.CurrentPhase.Store(DoSubscribeOver)
x.Cond.Broadcast()
}
// 启动Redis订阅协程,监听状态变更消息
go func() {
x.doSubscribe(ctx, rdb, fn)
}()
return x, nil
}
6.3 状态变更回调处理机制
go
// internal/msggateway/subscription.go - 状态变更回调实现
func (ws *WsServer) subscriberUserOnlineStatusChanges(ctx context.Context, userID string, platformIDs []int32) {
// 步骤1:检查是否需要同步状态到本地连接映射
// 设计思路:确保本地连接状态与Redis中的全局状态保持一致
if ws.clients.RecvSubChange(userID, platformIDs) {
log.ZDebug(ctx, "gateway receive subscription message and go back online",
"userID", userID, "platformIDs", platformIDs)
} else {
log.ZDebug(ctx, "gateway ignore user online status changes",
"userID", userID, "platformIDs", platformIDs)
}
// 步骤2:向所有订阅者推送该用户的状态变更通知
ws.pushUserIDOnlineStatus(ctx, userID, platformIDs)
}
6.4 LRU缓存更新逻辑
go
// pkg/rpccache/online.go - LRU缓存状态更新
func (o *OnlineCache) setHasUserOnline(userID string, platformIDs []int32) bool {
// 使用LRU的SetHas方法,返回是否实际存储到缓存
// 设计思路:
// 1. 如果用户已在缓存中,直接更新状态
// 2. 如果缓存未满,添加新用户状态
// 3. 如果缓存已满,按LRU策略淘汰最久未使用的用户
return o.lruCache.SetHas(userID, platformIDs)
}
// doSubscribe中的LRU模式处理逻辑
doMessage := func(message *redis.Message) {
userID, platformIDs, err := useronline.ParseUserOnlineStatus(message.Payload)
if err != nil {
log.ZError(ctx, "parseUserOnlineStatus error", err)
return
}
switch o.fullUserCache {
case false:
// LRU缓存模式:更新lruCache并调用回调函数
storageCache := o.setHasUserOnline(userID, platformIDs)
log.ZDebug(ctx, "OnlineCache setHasUserOnline",
"userID", userID, "platformIDs", platformIDs,
"storageCache", storageCache) // storageCache表示是否实际存储到缓存
if fn != nil {
// 执行外部回调函数,触发订阅推送
fn(ctx, userID, platformIDs)
}
}
}
6.5 本地连接状态同步检查
go
// internal/msggateway/user_map.go - 连接状态同步检查
func (u *UserMap) RecvSubChange(userID string, platformIDs []int32) bool {
u.Lock()
defer u.Unlock()
userConnList, ok := u.m[userID]
if !ok {
return false // 用户在本网关无连接
}
// 检查Redis状态与本地连接状态是否一致
onlineDeviceMap := make(map[int]struct{})
for _, deviceID := range platformIDs {
onlineDeviceMap[int(deviceID)] = struct{}{}
}
// 同步检查逻辑:
// 1. 检查本地连接但Redis中离线的设备
// 2. 检查Redis中在线但本地无连接的设备
needSync := false
for platformID := range userConnList {
if _, exists := onlineDeviceMap[platformID]; !exists {
needSync = true // 本地有连接但Redis中已离线
break
}
}
if !needSync {
for deviceID := range onlineDeviceMap {
if _, exists := userConnList[deviceID]; !exists {
needSync = true // Redis中在线但本地无连接
break
}
}
}
if needSync {
// 触发状态同步,推送到状态变更通道
u.push(UserState{
UserID: userID,
Online: platformIDs,
Offline: nil, // 离线平台会在msggateway内部逻辑中处理
})
}
return needSync
}
6.6 订阅者状态推送实现
go
// internal/msggateway/subscription.go - 推送状态变更给订阅者
func (ws *WsServer) pushUserIDOnlineStatus(ctx context.Context, userID string, platformIDs []int32) {
// 步骤1:获取订阅该用户的所有客户端
clients := ws.subscription.GetClient(userID)
if len(clients) == 0 {
return // 没有订阅者
}
// 步骤2:构建状态变更通知消息
onlineStatus, err := proto.Marshal(&sdkws.SubUserOnlineStatusTips{
Subscribers: []*sdkws.SubUserOnlineStatusElem{{
UserID: userID,
OnlinePlatformIDs: platformIDs,
}},
})
if err != nil {
log.ZError(ctx, "pushUserIDOnlineStatus proto.Marshal error", err)
return
}
// 步骤3:向每个订阅客户端推送状态变更通知
for _, client := range clients {
if err := client.PushUserOnlineStatus(onlineStatus); err != nil {
log.ZError(ctx, "PushUserOnlineStatus failed", err,
"subscriberUserID", client.UserID,
"subscriberPlatformID", client.PlatformID,
"changeUserID", userID,
"changePlatformIDs", platformIDs)
}
}
}
// internal/msggateway/client.go - 客户端推送实现
func (c *Client) PushUserOnlineStatus(data []byte) error {
resp := Resp{
ReqIdentifier: WsSubUserOnlineStatus, // 订阅状态变更响应标识
Data: data,
}
return c.writeBinaryMsg(resp)
}
状态管理核心流程图
flowchart TD
%% 用户连接入口
Start([用户WebSocket连接]) --> CreateClient[创建Client对象]
CreateClient --> RegisterChan[发送到registerChan]
RegisterChan --> RegisterClient[registerClient处理]
%% 用户状态检查
RegisterClient --> CheckUser{检查用户状态}
%% 新用户分支
CheckUser -->|新用户| CreateUserPlatform[创建UserPlatform对象]
CreateUserPlatform --> UpdateMetrics1[更新在线用户计数+1]
UpdateMetrics1 --> SetUserClient1[clients.Set添加连接]
%% 老用户分支
CheckUser -->|老用户| CheckPlatform{检查平台连接}
%% 同平台重复登录
CheckPlatform -->|同平台有连接| MultiTerminalCheck[多端登录策略检查]
MultiTerminalCheck --> KickOldClient{是否踢掉旧连接}
KickOldClient -->|踢掉| CloseOldConn[关闭旧连接]
KickOldClient -->|保留| SetUserClient2[替换为新连接]
CloseOldConn --> SetUserClient2
%% 新平台连接
CheckPlatform -->|新平台连接| UpdateMetrics2[更新连接数+1]
UpdateMetrics2 --> SetUserClient3[clients.Set添加连接]
%% 统一到状态变更推送
SetUserClient1 --> PushUserState[push UserState事件]
SetUserClient2 --> PushUserState
SetUserClient3 --> PushUserState
%% 状态变更处理
PushUserState --> ReceiveEvent[ChangeOnlineStatus接收事件]
ReceiveEvent --> HashShard[MD5哈希计算分片索引]
HashShard --> AddToBuffer[添加到对应缓冲区]
%% 批量处理决策
AddToBuffer --> BatchDecision{批量处理决策}
BatchDecision -->|缓冲区满| ImmediateSend[立即发送RPC请求]
BatchDecision -->|定时器触发| TimerSend[定时器强制发送]
BatchDecision -->|实时变更| RealtimeSend[实时发送请求]
%% RPC处理
ImmediateSend --> CreateBatchReq[创建批量RPC请求]
TimerSend --> CreateBatchReq
RealtimeSend --> CreateBatchReq
CreateBatchReq --> GenerateOpID[生成唯一操作ID]
GenerateOpID --> SetTimeout[设置5秒超时]
SetTimeout --> CallUserRPC[调用User RPC服务]
%% User RPC处理
CallUserRPC --> IterateStatus[遍历状态列表]
IterateStatus --> CallSetUserOnline[调用SetUserOnline]
%% Redis Lua脚本执行
CallSetUserOnline --> LuaScript[执行Redis Lua脚本]
LuaScript --> RecordCount1[ZCARD记录操作前成员数]
RecordCount1 --> CleanExpired[ZREMRANGEBYSCORE清理过期]
CleanExpired --> RemoveOffline[ZREM移除离线平台]
RemoveOffline --> RecordCount2[ZCARD记录移除后成员数]
RecordCount2 --> AddOnline[ZADD添加在线平台]
AddOnline --> SetExpire[EXPIRE设置键过期]
SetExpire --> RecordCount3[ZCARD记录最终成员数]
RecordCount3 --> CheckChange{检查是否有变更}
%% 状态变更检查
CheckChange -->|有变更| GetMembers[ZRANGE获取所有成员]
CheckChange -->|无变更| ReturnNoChange[返回无变更标志]
GetMembers --> PublishChange[PUBLISH发布状态变更]
%% Redis发布订阅分发
PublishChange --> PushSubscribe[Push服务订阅处理]
PublishChange --> GatewaySubscribe[Gateway服务订阅处理]
%% Push服务缓存更新
PushSubscribe --> ParseMessage1[解析状态变更消息]
ParseMessage1 --> CheckCacheMode1{检查缓存模式}
CheckCacheMode1 -->|全量缓存| UpdateMapCache[mapCache.Store更新]
%% Gateway LRU缓存更新
GatewaySubscribe --> ParseMessage2[解析状态变更消息]
ParseMessage2 --> CheckCacheMode2{检查缓存模式}
CheckCacheMode2 -->|LRU缓存| UpdateLRUCache[lruCache.SetHas更新]
UpdateLRUCache --> CallbackFunction[执行状态变更回调]
%% 订阅推送处理
CallbackFunction --> CheckLocalConsistency[检查本地连接一致性]
CheckLocalConsistency --> SyncCheck{状态是否一致}
SyncCheck -->|不一致| TriggerSync[触发重新同步]
SyncCheck -->|一致| GetSubscribers[获取该用户的订阅者]
TriggerSync --> PushUserState
GetSubscribers --> BuildNotification[构建状态变更通知]
BuildNotification --> PushToSubscribers[推送给所有订阅者]
%% Webhook回调
CallUserRPC --> ProcessWebhook[处理Webhook回调]
ProcessWebhook --> OnlineWebhook[处理上线Webhook]
OnlineWebhook --> OfflineWebhook[处理下线Webhook]
%% 续约机制
RenewalTimer[续约定时器每10分钟] --> CheckRenewalDeadline[检查续约截止时间]
CheckRenewalDeadline --> GetRenewalUsers[获取需续约用户列表]
GetRenewalUsers --> UpdateTimestamp[更新时间戳]
UpdateTimestamp --> PushUserState
%% 用户断开连接
UserDisconnect([用户断开连接]) --> UnregisterChan[发送到unregisterChan]
UnregisterChan --> UnregisterClient[unregisterClient处理]
UnregisterClient --> DeleteFromMap[从连接映射中删除]
DeleteFromMap --> CheckLastConnection{是否最后一个连接}
CheckLastConnection -->|是| DeleteUserRecord[删除用户记录]
CheckLastConnection -->|否| UpdateConnectionCount[更新连接计数]
DeleteUserRecord --> CleanSubscriptions[清理订阅关系]
UpdateConnectionCount --> CleanSubscriptions
CleanSubscriptions --> PushOfflineState[推送离线状态]
PushOfflineState --> PushUserState
%% 对象池回收
UnregisterClient --> ObjectPoolReturn[对象池回收Client]
%% 样式定义
classDef entryPoint fill:#e8f5e8,stroke:#4caf50,stroke-width:2px
classDef processNode fill:#e3f2fd,stroke:#2196f3,stroke-width:1px
classDef decisionNode fill:#fff3e0,stroke:#ff9800,stroke-width:1px
classDef storageNode fill:#f3e5f5,stroke:#9c27b0,stroke-width:1px
classDef exitPoint fill:#ffebee,stroke:#f44336,stroke-width:2px
class Start,UserDisconnect,RenewalTimer entryPoint
class CreateClient,RegisterClient,CreateUserPlatform,UpdateMetrics1,SetUserClient1,SetUserClient2,SetUserClient3,MultiTerminalCheck,CloseOldConn,UpdateMetrics2,PushUserState,ReceiveEvent,HashShard,AddToBuffer,ImmediateSend,TimerSend,RealtimeSend,CreateBatchReq,GenerateOpID,SetTimeout,IterateStatus,RecordCount1,CleanExpired,RemoveOffline,RecordCount2,AddOnline,SetExpire,RecordCount3,GetMembers,ParseMessage1,UpdateMapCache,ParseMessage2,UpdateLRUCache,CallbackFunction,CheckLocalConsistency,GetSubscribers,BuildNotification,PushToSubscribers,ProcessWebhook,OnlineWebhook,OfflineWebhook,CheckRenewalDeadline,GetRenewalUsers,UpdateTimestamp,UnregisterClient,DeleteFromMap,DeleteUserRecord,UpdateConnectionCount,CleanSubscriptions,PushOfflineState,ObjectPoolReturn processNode
class CheckUser,CheckPlatform,KickOldClient,BatchDecision,CheckChange,CheckCacheMode1,CheckCacheMode2,SyncCheck,CheckLastConnection decisionNode
class CallUserRPC,CallSetUserOnline,LuaScript,PublishChange,PushSubscribe,GatewaySubscribe storageNode
class ReturnNoChange,TriggerSync exitPoint
性能优化策略总结
缓存层次设计
缓存层级 | 存储位置 | 容量策略 | 更新频率 | 适用场景 |
---|---|---|---|---|
本地连接映射 | msggateway内存 | 实际连接数 | 实时更新 | 连接管理 |
Push全量缓存 | Push服务内存 | 全量用户 | Redis订阅 | 推送决策 |
LRU热点缓存 | msggateway内存 | 固定容量 | 访问驱动 | 状态查询 |
Redis主存储 | Redis集群 | 无限制 | 状态变更 | 持久化存储 |
批量处理策略
处理类型 | 触发条件 | 批次大小 | 时间窗口 | 性能收益 |
---|---|---|---|---|
状态变更 | 缓冲区满/定时 | 100条 | 1秒 | 减少RPC调用 |
续约请求 | 定时触发 | 无限制 | 10分钟 | 批量续约 |
订阅推送 | 状态变更 | 单用户 | 实时 | 降低延迟 |