OpenIM 源码深度解析系列(四):在线状态相关存储结构

在线状态相关存储结构

概述

OpenIM在线状态系统采用多层存储架构,从WebSocket网关的内存存储到Redis集中缓存,再到推送服务的本地缓存,形成完整的状态管理链路。系统核心设计思路是:单节点状态聚合 → 全局状态同步 → 分布式缓存 → 业务决策,确保在分布式环境下的状态一致性和高性能查询。

服务架构概览

graph TB subgraph "客户端层" Client1[iOS客户端] Client2[Android客户端] Client3[Web客户端] Client4[PC客户端] end subgraph "网关服务层" subgraph "msggateway服务" WS[WebSocket网关] UserMap[用户连接映射
user_map.go] Subscription[订阅关系管理
subscription.go] OnlineProcessor[状态处理器
online.go] OnlineCache[订阅在线缓存
rpccache/online.go] end end subgraph "核心服务层" subgraph "msg-transfer服务" Transfer[消息传输服务] end subgraph "push服务" PushHandler[推送处理器
push_handler.go] PushOnline[推送在线缓存
rpccache/online.go] end subgraph "user服务" UserRPC[用户RPC服务
user.go] UserOnlineAPI[在线状态接口
online.go] UserOnlineCache[在线状态缓存
redis.NewUserOnline] end end subgraph "缓存模块" subgraph "Redis集群" RedisOnline[在线状态缓存
redis/online.go] RedisPubSub[发布订阅
online_change频道] end subgraph "本地缓存" RPCCache[RPC缓存
rpccache/online.go] LRUCache[LRU缓存
SlotLRU/LayLRU] end end subgraph "外部服务" APNs[苹果推送] FCM[谷歌推送] JPush[极光推送] end %% 连接关系 Client1 -.-> WS Client2 -.-> WS Client3 -.-> WS Client4 -.-> WS WS --> UserMap WS --> Subscription WS --> OnlineProcessor OnlineProcessor --> UserRPC UserRPC --> UserOnlineAPI UserOnlineAPI --> UserOnlineCache UserOnlineCache --> RedisOnline RedisOnline --> RedisPubSub RedisPubSub --> RPCCache RedisPubSub --> UserMap Transfer --> PushHandler PushHandler --> PushOnline PushOnline --> RPCCache OnlineCache --> RPCCache OnlineCache --> Subscription RPCCache --> LRUCache PushHandler --> APNs PushHandler --> FCM PushHandler --> JPush %% 样式设置 style WS fill:#e1f5fe style RedisOnline fill:#f3e5f5 style RPCCache fill:#e8f5e8 style PushHandler fill:#fff3e0

一、msggateway中的user_map.go存储结构

核心功能:单节点连接关系管理

功能定位:管理单个msggateway节点内用户与平台连接的映射关系,是WebSocket连接的内存索引。

设计思路

  • 单节点范围:每个msggateway实例独立维护自己的连接映射
  • 三层映射:用户ID → 平台连接信息 → 具体连接对象
  • 实时更新:连接建立/断开时立即更新映射关系
  • 状态事件源:作为用户状态变更事件的发起点

1.1 UserMap 用户连接映射存储

go 复制代码
type userMap struct {
    lock sync.RWMutex             // 读写锁,保护并发访问
    data map[string]*UserPlatform // 用户ID到平台连接的映射
    ch   chan UserState           // 状态变更通知通道(容量10000)
}

存储层次:

  • 第一层 : userID -> *UserPlatform (用户维度聚合)
  • 第二层 : UserPlatform.Clients -> []*Client (平台维度列表)
  • 第三层: 每个Client包含平台ID和连接信息 (连接实例)

关键操作

  • Set(): 添加新连接,触发上线事件
  • DeleteClients(): 删除连接,触发下线事件
  • Get(): 查询特定平台连接,支持推送路由
  • GetAll(): 获取用户所有连接,用于状态查询

1.2 UserPlatform 平台连接聚合

go 复制代码
type UserPlatform struct {
    Time    time.Time // 最后更新时间(用于续约判断)
    Clients []*Client // 该用户的所有客户端连接
}

聚合策略

  • 同一用户不同平台的连接统一管理
  • 支持单用户多平台并发连接(如手机+PC同时在线)
  • 时间戳用于定时续约机制的判断依据

二、msggateway中的subscription.go存储结构

核心功能:设备端订阅关系管理

功能定位:管理设备端对其他用户在线状态的订阅关系,实现好友在线状态实时通知功能。

应用场景

  • 好友列表显示在线状态
  • 好友上线/下线通知
  • 聊天界面实时状态更新
  • 业务层订阅特定用户状态

2.1 Subscription 订阅关系管理

go 复制代码
type Subscription struct {
    lock    sync.RWMutex          // 读写锁,保护并发访问
    userIDs map[string]*subClient // 用户ID -> 订阅客户端集合映射
}

双向映射设计

  • 正向映射:被订阅用户ID → 订阅该用户的所有客户端
  • 反向映射:每个客户端维护自己订阅的用户ID集合
  • 一致性保证:添加/删除订阅时同时更新两个方向的映射

2.2 subClient 订阅客户端集合

go 复制代码
type subClient struct {
    clients map[string]*Client // key: 客户端连接地址, value: 客户端连接
}

集合管理

  • 使用连接地址作为唯一标识,避免重复订阅
  • 支持同一用户多个设备同时订阅
  • 客户端断开时自动清理订阅关系

2.3 Client 客户端订阅状态

go 复制代码
type Client struct {
    subLock    *sync.Mutex         // 订阅锁
    subUserIDs map[string]struct{} // 订阅的用户ID集合
    // ... 其他连接相关字段
}

订阅操作

  • Sub(): 批量添加/删除订阅,支持原子操作
  • DelClient(): 客户端断开时清理所有订阅关系
  • GetClient(): 查询订阅特定用户的所有客户端

订阅关系存储:

  • 正向映射 : userID -> 订阅该用户的所有客户端
  • 反向映射: 每个客户端维护自己订阅的用户ID集合
  • 双向维护: 确保订阅关系的一致性

三、msggateway中的online.go存储结构

核心功能:状态变更聚合与同步

功能定位:将单节点msggateway的用户状态变更进行批量合并处理,然后同步到User RPC服务,最终存储到Redis全局缓存。

设计目标

  1. 定时续约用户在线状态,防止缓存过期 - 确保Redis中的状态不会因为TTL到期而丢失
  2. 实时处理用户状态变更事件 - 响应连接建立/断开的即时状态变化
  3. 批量合并状态更新请求,提高性能 - 减少网络请求次数,提升系统吞吐量

3.1 状态变更处理器架构

go 复制代码
// ChangeOnlineStatus 核心状态处理器
func (ws *WsServer) ChangeOnlineStatus(concurrent int) {
    // 并发处理通道(默认4个并发处理器)
    requestChs := make([]chan *pbuser.SetUserOnlineStatusReq, concurrent)
    // 状态变更缓冲区(每个处理器独立缓冲)
    changeStatus := make([][]UserState, concurrent)
}

并发处理设计

  • 哈希分片:基于用户ID的MD5哈希分配到不同处理器
  • 负载均衡:避免热点用户导致的处理瓶颈
  • 批量优化:每个处理器独立缓冲和合并状态变更

3.2 状态缓冲机制

go 复制代码
// 状态变更缓冲结构
type StatusBuffer struct {
    buffer   []UserState                    // 状态变更缓冲区(容量100)
    requests chan *pbuser.SetUserOnlineStatusReq // 请求通道(容量64)
    ticker   *time.Ticker                   // 定时器(1秒间隔)
}

缓冲策略:

  1. 批量合并: 相同用户的多次状态变更合并为一次RPC调用
  2. 定时刷新: 每秒强制推送积累的状态变更,保证实时性
  3. 容量触发: 缓冲区满时立即推送,避免内存积压
  4. 哈希分片: 基于用户ID哈希分配到不同处理器,保证顺序性

三种事件处理

  1. 定时合并推送:每秒触发,确保状态及时同步
  2. 定时续约:防止Redis缓存过期,维持在线状态
  3. 实时状态变更:响应连接变化,立即处理状态更新

3.3 续约机制设计

go 复制代码
// 续约时间设置为缓存过期时间的1/3
const renewalTime = cachekey.OnlineExpire / 3
renewalTicker := time.NewTicker(renewalTime)

续约逻辑

  • 续约间隔:缓存过期时间的1/3,确保多次续约机会
  • 续约对象:上次更新时间超过续约间隔的在线用户
  • 续约效果:重新设置Redis中的过期时间,保持在线状态

四、User RPC服务状态处理

4.1 User服务架构

User服务作为状态变更链路中的关键环节,负责接收msggateway的状态变更请求,并操作Redis进行状态持久化。

go 复制代码
// user/user.go 服务结构
type userServer struct {
    online cache.OnlineCache // 在线状态缓存接口,实际为redis.NewUserOnline
    // ... 其他字段
}

// 服务初始化
func Start() {
    u := &userServer{
        online: redis.NewUserOnline(rdb), // 初始化Redis在线状态缓存
        // ... 其他初始化
    }
}

4.2 状态处理接口

go 复制代码
// user/online.go 状态处理方法
func (s *userServer) SetUserOnlineStatus(ctx context.Context, req *pbuser.SetUserOnlineStatusReq) (*pbuser.SetUserOnlineStatusResp, error) {
    // 遍历每个用户的状态信息进行更新
    for _, status := range req.Status {
        if err := s.online.SetUserOnline(ctx, status.UserID, status.Online, status.Offline); err != nil {
            return nil, err
        }
    }
    return &pbuser.SetUserOnlineStatusResp{}, nil
}

调用链路:

  1. msggateway状态缓冲器 → User RPC服务
  2. User服务接收状态变更请求
  3. 调用s.online.SetUserOnline()操作Redis
  4. Redis执行Lua脚本更新ZSET状态
  5. 发布状态变更到Redis订阅频道

五、全局缓存redis/online.go存储结构

核心功能:全局在线状态存储

功能定位:作为OpenIM系统的在线状态真实数据源,存储所有设备的在线关系,包括用户ID、平台ID和过期时间。

存储特点

  • 全局唯一:所有msggateway节点共享同一份状态数据
  • 分布式一致性:通过Redis保证多节点状态一致
  • TTL自动过期:基于时间戳自动清理过期状态
  • 原子操作:使用Lua脚本保证状态更新的原子性

5.1 Redis ZSET 在线状态存储

makefile 复制代码
数据类型: Sorted Set (ZSET)
键格式: ONLINE:{userID}
成员(member): 平台ID (platformID)
分值(score): 过期时间戳 (Unix timestamp)

示例数据:
ONLINE:user123
- 成员"1" 分值1640995200 (iOS平台,过期时间)
- 成员"2" 分值1640995200 (Android平台,过期时间)
- 成员"3" 分值1640995200 (Web平台,过期时间)

ZSET优势

  • 自动排序:按过期时间戳排序,便于批量清理过期数据
  • 范围查询:支持按时间范围快速筛选有效状态
  • 原子操作:Redis原生支持ZSET的原子更新操作

5.2 Lua脚本原子操作

lua 复制代码
-- 原子性状态更新脚本
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

-- 添加新的在线平台(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

脚本功能

  1. 过期清理:自动移除score小于当前时间的过期平台
  2. 离线处理:移除指定的离线平台ID
  3. 在线更新:添加新的在线平台,设置过期时间
  4. 变更检测:判断状态是否发生实际变化
  5. 发布控制:只有状态真正变更时才触发后续通知

六、Redis发布订阅机制

核心功能:状态变更实时通知

功能定位:当设备的在线状态变更时,通过Redis发布订阅机制实时通知所有订阅者,确保分布式环境下的状态一致性。

makefile 复制代码
频道名称: online_change
消息格式: platformID1:platformID2:...:userID
示例: "1:2:3:user123" (用户user123在平台1,2,3上线)
空状态: ":user123" (用户user123完全离线)

6.1 发布端 (Redis/online.go)

go 复制代码
// 状态变更发布逻辑
if platformIDs[len(platformIDs)-1] != "0" {
    // 构建通知消息:平台ID列表 + 用户ID
    platformIDs[len(platformIDs)-1] = userID
    msg := strings.Join(platformIDs, ":")
    
    // 发布到状态变更通知频道
    s.rdb.Publish(ctx, s.channelName, msg)
}

发布时机

  • 用户上线:新平台连接建立
  • 用户离线:平台连接断开
  • 状态续约:重新设置过期时间
  • 平台切换:同一用户不同平台间的切换

6.2 订阅端处理机制

go 复制代码
// 消息解析和处理
userID, platformIDs, err := useronline.ParseUserOnlineStatus(message.Payload)
// 根据缓存策略更新本地缓存
// 触发回调函数处理业务逻辑

订阅者类型

  1. msggateway实例:更新本地订阅关系,推送好友状态通知
  2. push服务实例:更新本地缓存,优化推送决策
  3. 其他业务服务:根据需要订阅状态变更

七、订阅处理 (rpccache/online.go + subscriber.go)

核心功能:分布式在线状态缓存

功能定位:从Redis全量拉取或基于LRU缓存维护用户在线状态,同时订阅Redis发布的状态变更消息,实时调整本地缓存。

缓存策略选择

  • 全量缓存模式:适合用户规模较小的场景,内存换查询性能
  • LRU缓存模式:适合大规模用户场景,固定内存使用,热点数据优先

7.1 OnlineCache 本地缓存结构

go 复制代码
type OnlineCache struct {
    client *rpcli.UserClient // 用户服务RPC客户端
    
    // 缓存策略选择
    fullUserCache bool
    lruCache lru.LRU[string, []int32]          // LRU缓存(固定容量)
    mapCache *cacheutil.Cache[string, []int32] // 全量缓存(线性增长)
    
    // 阶段同步机制
    Lock         *sync.Mutex
    Cond         *sync.Cond
    CurrentPhase atomic.Uint32
}

初始化流程

  1. 全量同步:从Redis加载所有在线用户状态
  2. 订阅启动:开始监听Redis状态变更消息
  3. 服务就绪:标记缓存可用,开始对外提供服务

7.2 三阶段初始化机制

diff 复制代码
阶段1: Begin (开始阶段)
- 初始化缓存结构
- 准备数据同步

阶段2: DoOnlineStatusOver (在线状态初始化完成)
- 完成全量在线状态同步
- 本地缓存构建完成

阶段3: DoSubscribeOver (订阅初始化完成)
- 启动Redis订阅监听
- 系统完全就绪,可对外服务

同步等待机制

  • 推送服务等待第3阶段完成后才开始消费Kafka消息
  • 确保推送决策时在线状态数据的完整性和准确性

7.3 缓存策略对比

特性 全量缓存模式 LRU缓存模式
存储结构 map[string][]int32 SlotLRU[string, []int32]
内存使用 线性增长,无上限 固定上限,可控制
查询性能 O(1),始终命中 缓存命中O(1),miss需RPC
初始化成本 需全量加载,耗时较长 立即可用,按需加载
适用场景 中小规模用户(<10万) 大规模用户(>10万)
状态更新 直接更新map 更新LRU并调整顺序

7.4 订阅消息处理

go 复制代码
// 状态变更消息处理流程
func (o *OnlineCache) handleStatusChange(userID string, platformIDs []int32) {
    if o.fullUserCache {
        // 全量缓存:直接更新
        o.mapCache.Set(userID, platformIDs)
    } else {
        // LRU缓存:更新存在的记录
        o.lruCache.Set(userID, platformIDs)
    }
    
    // 触发回调通知(如果配置)
    if o.callback != nil {
        o.callback(ctx, userID, platformIDs)
    }
}

八、msggateway引用 (init.go + online.go)

核心功能:订阅关系状态同步

功能定位:订阅全局的设备在线变更消息,通过本地的订阅关系管理,将状态变更同步给相关的订阅设备端。

业务流程

  1. 状态监听:订阅Redis发布的在线状态变更
  2. 关系检查:查询本地是否有客户端订阅该用户
  3. 状态推送:向订阅客户端推送好友上线/下线通知

8.1 初始化配置

go 复制代码
// msggateway/init.go
longServer.online, err = rpccache.NewOnlineCache(
    srv.userClient, 
    nil, 
    rdb, 
    false, // 使用LRU缓存模式(msggateway通常处理连接数有限)
    longServer.subscriberUserOnlineStatusChanges // 设置状态变更回调
)

配置说明

  • LRU模式:msggateway主要关注已连接用户的状态,使用LRU更高效
  • 回调设置:设置状态变更回调函数,实现订阅通知功能

8.2 状态变更处理器启动

go 复制代码
// 启动4个并发处理器
go longServer.ChangeOnlineStatus(4)

并发设计

  • 4个处理器:平衡处理能力和资源消耗
  • 哈希分片:确保同一用户的操作顺序性
  • 批量合并:减少RPC调用次数

8.3 回调函数集成

go 复制代码
// 状态变更回调
func (ws *WsServer) subscriberUserOnlineStatusChanges(ctx context.Context, userID string, platformIDs []int32) {
    // 检查本地连接状态(可能是其他节点的状态变更)
    if ws.clients.RecvSubChange(userID, platformIDs) {
        log.ZDebug(ctx, "gateway receive subscription message and go back online", "userID", userID, "platformIDs", platformIDs)
        // 推送给订阅者
        ws.pushUserIDOnlineStatus(ctx, userID, platformIDs)
    } else {
        log.ZDebug(ctx, "gateway ignore user online status changes", "userID", userID, "platformIDs", platformIDs)
    }
}

处理逻辑

  1. 本地检查RecvSubChange()检查本地是否还有该用户的连接
  2. 订阅推送pushUserIDOnlineStatus()向订阅该用户的客户端推送状态
  3. 日志记录:记录处理结果,便于调试和监控

九、push服务引用 (push.go + online.go)

核心功能:推送决策缓存

功能定位:在push服务中维护所有设备的在线状态本地缓存,订阅状态变更做实时调整,该缓存主要用于推送时判断是走在线推送还是离线推送。

决策价值

  • 在线推送:WebSocket实时推送,延迟低,体验好
  • 离线推送:APNs/FCM/极光推送,确保消息送达
  • 性能优化:避免每次推送都查询Redis,提升推送效率

9.1 Push服务初始化

go 复制代码
// push/push_handler.go
consumerHandler.onlineCache, err = rpccache.NewOnlineCache(
    consumerHandler.userClient, 
    consumerHandler.groupLocalCache, 
    rdb, 
    config.RpcConfig.FullUserCache, // 根据配置选择缓存策略
    nil // 无状态变更回调函数(只关注查询)
)

配置特点

  • 策略可配置:支持全量缓存和LRU缓存两种模式
  • 无回调函数:push服务只需要查询状态,不需要处理订阅推送
  • 群组缓存集成:结合群组成员缓存,优化群聊推送

9.2 推送决策集成

go 复制代码
// 获取用户在线状态用于推送决策
onlineUserIDs, offlineUserIDs, err := c.onlineCache.GetUsersOnline(ctx, pushToUserIDs)

// 在线用户:WebSocket推送
if len(onlineUserIDs) > 0 {
    result, err = c.onlinePusher.GetConnsAndOnlinePush(ctx, msg, onlineUserIDs)
}

// 离线用户:离线推送
for _, userID := range offlineUserIDs {
    // 触发离线推送逻辑
    result = append(result, &msggateway.SingleMsgToUserResults{
        UserID: userID,
        OnlinePush: false, // 标记为离线状态
    })
}
相关推荐
星星电灯猴23 分钟前
Charles抓包工具深度解析:如何高效调试HTTPHTTPS请求与API接口
后端
isfox27 分钟前
Hadoop 版本进化论:从 1.0 到 2.0,架构革命全解析
大数据·后端
normaling39 分钟前
四、go语言指针
后端
yeyong1 小时前
用springboot开发一个snmp采集程序,并最终生成拓扑图 (二)
后端
掉鱼的猫2 小时前
Solon AI 五步构建 RAG 服务:2025 最新 AI + 向量数据库实战
java·redis·后端
HyggeBest2 小时前
Mysql之undo log、redo log、binlog日志篇
后端·mysql
java金融2 小时前
FactoryBean 和BeanFactory的傻傻的总是分不清?
java·后端
独立开阀者_FwtCoder2 小时前
Nginx 部署负载均衡服务全解析
前端·javascript·后端
独立开阀者_FwtCoder2 小时前
Nginx 通过匹配 Cookie 将请求定向到特定服务器
java·vue.js·后端