OpenIM 源码深度解析系列(五):分布式在线状态管理的完整实现

449 阅读27分钟

分布式在线状态管理的完整实现

概述

OpenIM的在线状态管理是一个精心设计的分布式系统,涵盖从单节点连接管理到跨服务状态同步的完整链路。该系统通过六个关键阶段实现了高效、可靠的用户在线状态管理,支撑着OpenIM的实时消息推送和用户体验。

系统架构特点

🔄 事件驱动架构

  • msggateway采用事件循环处理连接生命周期
  • 基于通道的异步消息传递机制
  • 非阻塞的状态变更处理

⚡ 多级缓存策略

  • 本地连接映射:实时维护用户-设备连接关系
  • Redis全局缓存:跨节点的状态一致性存储
  • Push服务全量缓存:快速在线状态查询
  • msggateway LRU缓存:热点用户状态优化

🚀 批量处理优化

  • 状态变更的智能合并与分片
  • 定时续约机制防止状态过期
  • 并发RPC调用提升处理吞吐量

📡 分布式协调

  • Redis发布订阅实现状态广播
  • 跨节点多端登录冲突处理
  • 用户状态订阅与实时推送

六大核心阶段

  1. msggateway本地连接映射 - 单节点用户连接状态管理
  2. 事件变更同步机制 - 状态变更的聚合与批量处理
  3. User RPC服务处理 - 状态数据的持久化存储
  4. Push服务全量缓存 - 推送决策的性能优化
  5. 用户状态订阅管理 - 实时状态变更通知
  6. 全局状态消息订阅 - 分布式状态一致性保证

阶段一:msggateway中的本地用户连接映射

1.1 网关启动监听机制

核心启动流程

在msggateway服务启动时,WsServer.Run()方法会启动一个核心的事件监听协程,负责处理所有的连接生命周期事件:

// 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 - 新连接注册核心逻辑
// 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. 状态检查优先:先检查现有状态,再决定处理策略
  2. 统计数据同步:实时更新监控指标,便于运维监控
  3. 集群协调:主动同步状态到其他节点,保证分布式一致性

1.3 用户映射操作详解

UserMap.Set - 状态变更的起点
// 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方法 - 状态变更事件发布
// 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 - 连接断开处理
// 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 - 精确连接移除
// 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
}

核心设计思想:

  1. 精确删除:基于连接地址进行精确匹配删除
  2. 状态完整性:同时记录在线和离线状态变更
  3. 内存管理:及时清理无效连接,避免内存泄漏
  4. 事件触发:删除操作同样会触发状态变更通知

阶段二:msggateway中的事件变更同步

2.1 状态同步协程启动

ChangeOnlineStatus - 核心状态处理器
// 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)
    
    // ... 后续逻辑
}

设计核心思想:

  1. 并发处理:通过多个协程并行处理状态变更,提升吞吐量
  2. 哈希分片:确保同一用户的操作顺序性
  3. 批量缓冲:减少RPC调用次数,提升性能

2.2 续约机制深度解析

为什么需要续约机制?
// Redis TTL机制的挑战:
// 1. Redis中的用户在线状态设置了30分钟的TTL
// 2. 如果服务突然宕机,用户状态会一直保持"在线"直到TTL过期
// 3. 这期间离线用户可能收不到离线推送通知
// 4. 续约机制确保长连接用户的状态不会意外过期

// 续约间隔计算
const renewalTime = cachekey.OnlineExpire / 3  // 10分钟续约一次
续约处理逻辑
// 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 - 批量状态获取
// 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
}

续约机制设计亮点:

  1. 时间窗口控制:只对超过时间阈值的用户进行续约
  2. 批量处理:一次处理多个用户的续约请求
  3. 时间戳更新:避免重复续约同一用户

2.3 批量合并策略

pushUserState - 智能分片与缓冲
// 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 - 定时强制刷新
// 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 三种触发机制

主事件循环
// 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)
    }
}

三种触发机制的设计思想:

  1. 实时响应:用户连接变化立即处理
  2. 定时保证:每秒强制刷新确保不丢失
  3. 续约保活:定期续约防止缓存过期

2.5 并发RPC调用

doRequest - 执行状态更新
// 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)
        }
    }
}
并发处理器启动
// online.go - 启动并发处理协程
for i := 0; i < concurrent; i++ {
    go func(ch <-chan *pbuser.SetUserOnlineStatusReq) {
        // 持续处理通道中的请求,直到通道关闭
        for req := range ch {
            doRequest(req)
        }
    }(requestChs[i])
}

并发设计亮点:

  1. 操作ID追踪:每个请求都有唯一ID,便于问题排查
  2. 超时控制:5秒超时避免长时间阻塞
  3. Webhook集成:状态变更自动触发外部回调

阶段三:User RPC服务处理

3.1 User服务初始化

Redis在线缓存初始化
// 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 - 批量状态更新入口
// 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
}

处理特点:

  1. 批量处理:一次请求可以更新多个用户状态
  2. 循环调用:逐个调用底层Redis操作
  3. 错误传播:任何一个操作失败都会返回错误

3.3 Redis状态操作详解

SetUserOnline - 核心状态更新逻辑
// 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服务在启动时建立了完整的在线状态本地缓存体系,通过全量加载和增量订阅相结合的方式,确保状态数据的完整性和实时性。

// 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 全量缓存模式核心数据结构

// 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核心实现
// 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底层实现分析
// 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方法的性能与一致性问题分析

一、性能与效率问题

  1. 遍历耗时过长

    • 问题:当数据集超大(如百万级Key)时,即使分批扫描,多次网络IO和遍历操作仍显著增加总耗时
    • 影响:可能拖慢客户端响应,导致初始化阶段过长
    • 优化方案:调整COUNT参数(建议100~1000),平衡单次返回量与服务端负载
  2. 服务端负载压力

    • 问题:COUNT值过大会令单次SCAN逼近KEYS的阻塞风险;过小则增加迭代次数
    • 建议:根据数据集规模压测选择合理COUNT值,避免极端设置

二、结果集的不确定性

  1. 重复或遗漏数据

    • 原因:SCAN基于游标增量遍历哈希桶,若遍历中触发Rehash(扩容/缩容),可能重复扫描部分Key或遗漏迁移中的Key
    • 应对策略:客户端需处理重复Key(如用Set去重),缩容场景建议避免同时写入
  2. 非实时快照

    • 问题:SCAN不保证返回遍历开始时的完整快照。若遍历期间Key过期或被删,可能缺失;新增Key可能被纳入
    • 影响:适合统计等容忍偏差场景,不适合强一致性需求

4.4 初始化与订阅的并发协作机制

// 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 订阅管理器数据结构设计

// 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 客户端订阅状态管理

// 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 订阅消息处理流程

// 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 订阅逻辑核心实现

// 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 订阅关系管理详细实现

// 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 订阅关系清理机制

// 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缓存模式初始化

// 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缓存模式的核心逻辑

// 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 状态变更回调处理机制

// 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缓存更新逻辑

// 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 本地连接状态同步检查

// 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 订阅者状态推送实现

// 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分钟批量续约
订阅推送状态变更单用户实时降低延迟