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

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

概述

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()方法会启动一个核心的事件监听协程,负责处理所有的连接生命周期事件:

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

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
}

核心设计思想:

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

阶段二: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)
    
    // ... 后续逻辑
}

设计核心思想:

  1. 并发处理:通过多个协程并行处理状态变更,提升吞吐量
  2. 哈希分片:确保同一用户的操作顺序性
  3. 批量缓冲:减少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
}

续约机制设计亮点:

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

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)
    }
}

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

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

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])
}

并发设计亮点:

  1. 操作ID追踪:每个请求都有唯一ID,便于问题排查
  2. 超时控制:5秒超时避免长时间阻塞
  3. 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
}

处理特点:

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

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方法的性能与一致性问题分析

一、性能与效率问题

  1. 遍历耗时过长

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

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

二、结果集的不确定性

  1. 重复或遗漏数据

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

    • 问题: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分钟 批量续约
订阅推送 状态变更 单用户 实时 降低延迟
相关推荐
码小凡1 小时前
优雅!用了这两款插件,我成了整个公司代码写得最规范的码农
java·后端
星星电灯猴2 小时前
Charles抓包工具深度解析:如何高效调试HTTPHTTPS请求与API接口
后端
isfox2 小时前
Hadoop 版本进化论:从 1.0 到 2.0,架构革命全解析
大数据·后端
normaling2 小时前
四、go语言指针
后端
yeyong2 小时前
用springboot开发一个snmp采集程序,并最终生成拓扑图 (二)
后端
掉鱼的猫3 小时前
Solon AI 五步构建 RAG 服务:2025 最新 AI + 向量数据库实战
java·redis·后端
HyggeBest3 小时前
Mysql之undo log、redo log、binlog日志篇
后端·mysql
java金融3 小时前
FactoryBean 和BeanFactory的傻傻的总是分不清?
java·后端
独立开阀者_FwtCoder4 小时前
Nginx 部署负载均衡服务全解析
前端·javascript·后端