在线状态相关存储结构
概述
OpenIM在线状态系统采用多层存储架构,从WebSocket网关的内存存储到Redis集中缓存,再到推送服务的本地缓存,形成完整的状态管理链路。系统核心设计思路是:单节点状态聚合 → 全局状态同步 → 分布式缓存 → 业务决策,确保在分布式环境下的状态一致性和高性能查询。
服务架构概览
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全局缓存。
设计目标:
- 定时续约用户在线状态,防止缓存过期 - 确保Redis中的状态不会因为TTL到期而丢失
- 实时处理用户状态变更事件 - 响应连接建立/断开的即时状态变化
- 批量合并状态更新请求,提高性能 - 减少网络请求次数,提升系统吞吐量
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秒间隔)
}
缓冲策略:
- 批量合并: 相同用户的多次状态变更合并为一次RPC调用
- 定时刷新: 每秒强制推送积累的状态变更,保证实时性
- 容量触发: 缓冲区满时立即推送,避免内存积压
- 哈希分片: 基于用户ID哈希分配到不同处理器,保证顺序性
三种事件处理:
- 定时合并推送:每秒触发,确保状态及时同步
- 定时续约:防止Redis缓存过期,维持在线状态
- 实时状态变更:响应连接变化,立即处理状态更新
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
}
调用链路:
- msggateway状态缓冲器 → User RPC服务
- User服务接收状态变更请求
- 调用
s.online.SetUserOnline()
操作Redis - Redis执行Lua脚本更新ZSET状态
- 发布状态变更到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
脚本功能:
- 过期清理:自动移除score小于当前时间的过期平台
- 离线处理:移除指定的离线平台ID
- 在线更新:添加新的在线平台,设置过期时间
- 变更检测:判断状态是否发生实际变化
- 发布控制:只有状态真正变更时才触发后续通知
六、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)
// 根据缓存策略更新本地缓存
// 触发回调函数处理业务逻辑
订阅者类型:
- msggateway实例:更新本地订阅关系,推送好友状态通知
- push服务实例:更新本地缓存,优化推送决策
- 其他业务服务:根据需要订阅状态变更
七、订阅处理 (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
}
初始化流程:
- 全量同步:从Redis加载所有在线用户状态
- 订阅启动:开始监听Redis状态变更消息
- 服务就绪:标记缓存可用,开始对外提供服务
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)
核心功能:订阅关系状态同步
功能定位:订阅全局的设备在线变更消息,通过本地的订阅关系管理,将状态变更同步给相关的订阅设备端。
业务流程:
- 状态监听:订阅Redis发布的在线状态变更
- 关系检查:查询本地是否有客户端订阅该用户
- 状态推送:向订阅客户端推送好友上线/下线通知
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)
}
}
处理逻辑:
- 本地检查 :
RecvSubChange()
检查本地是否还有该用户的连接 - 订阅推送 :
pushUserIDOnlineStatus()
向订阅该用户的客户端推送状态 - 日志记录:记录处理结果,便于调试和监控
九、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, // 标记为离线状态
})
}