Documentation
¶
Index ¶
- Variables
- func FlushDefaultLogger() error
- func Initialize(ctx context.Context, hub StoreTarget, redisClient redis.UniversalClient, ...) error
- type ConnectionAuthenticator
- type ConnectionClaims
- type ConnectionQualityStore
- type ConnectionStore
- type DeliverEnvelope
- type DeliverIntent
- type EventBus
- type EventBusTopic
- type EventHandler
- type GroupStore
- type HubStats
- type Logger
- type MessageQueue
- type MessageRole
- type MessageSink
- type OfflineMessageFilter
- type OfflineQueue
- type OfflineStore
- type OnlineStore
- type RouteAdvisor
- type StoreDeps
- type StoreHooks
- type StoreTarget
- type WorkloadStore
Constants ¶
This section is empty.
Variables ¶
var ErrOfflineStorageSkipped = errors.New("offline storage skipped for message type")
ErrOfflineStorageSkipped 表示消息类型被策略排除,不能将跳过视为保存成功
Functions ¶
func FlushDefaultLogger ¶
func FlushDefaultLogger() error
FlushDefaultLogger 刷出默认日志器的缓冲
默认日志器未被构造过时为 no-op(不触发 Once,避免为了刷缓冲反而建出 writer 与其 flusher goroutine)。可重复调用。 关停收口见 hub.SafeShutdown;panic 路径不依赖本函数——ConsoleWriter 队列满时 自行降级为持锁同步直写(writeSync),故进程崩溃前的 panic 日志不经缓冲丢失。
func Initialize ¶
func Initialize( ctx context.Context, hub StoreTarget, redisClient redis.UniversalClient, db *gorm.DB, hooks ...StoreHooks, ) error
Initialize 装配全部存储后端
参数:
- ctx: 装配超时上下文(用于 Redis Ping 连通性校验)
- hub: 注入目标(StoreTarget 能力面)
- redisClient / db: 基础设施句柄,由调用方持有,本函数不接管其生命周期
- hooks: 各适配器提供的装配钩子,可为 nil(表示该后端未部署)
返回:
- error: 前置校验失败或任一 hook 装配失败
Types ¶
type ConnectionAuthenticator ¶
type ConnectionAuthenticator interface {
// Authenticate 从 HTTP 请求中提取并解密 token
// 返回解密后的连接身份声明;token 不存在/无效/篡改/过期时返回 error
Authenticate(r *http.Request) (*ConnectionClaims, error)
}
ConnectionAuthenticator 连接鉴权器 SPI 从 HTTP 握手请求中提取并解密连接凭证,返回连接身份声明 内置实现见 transport 包 TokenAuthenticator(AES-GCM 对称加解密); 业务侧可实现本接口替换默认鉴权(如 OAuth/私有 Token),经 Hub 注入
type ConnectionClaims ¶
type ConnectionClaims struct {
UserID string `json:"uid"` // 用户ID(必填)
UserType string `json:"utp,omitempty"` // 用户类型(默认 visitor)
DeviceID string `json:"did,omitempty"` // 设备ID
AppID string `json:"aid,omitempty"` // 应用ID(最上层隔离维度,默认 "__default_app__")
Namespace string `json:"tid,omitempty"` // 命名空间ID(默认 "default",用于命名空间隔离与消息过滤)
GroupID string `json:"gid,omitempty"` // 群组ID(可选,支持逗号分隔多群组"g1,g2,g3";用于群组消息过滤与连接时自动加入)
ExpiresAt int64 `json:"exp,omitempty"` // 过期时间(Unix 秒;签发时自动填充,解密后校验)
}
ConnectionClaims 连接 Token 的身份声明(加密载荷) 将原本明文暴露的 user_id/user_type/device_id/app_id/namespace/group_id 加密到 token 中 字段名采用短缩写以减小 token 体积 注意:
- app_id 为应用ID(最上层隔离维度,默认 "__default_app__")
- group_id 为连接时自动加入的成员组标识(支持逗号分隔多群组"g1,g2,g3";空则加入默认组)
- 群组成员关系的后续变更仍由业务层 API 管理
type ConnectionQualityStore ¶
type ConnectionQualityStore interface {
// Upsert 创建或更新质量记录(首次连接建初始零值行 QualityScore=100,重连 reconnect_count+1)
Upsert(ctx context.Context, quality *models.ConnectionQuality) error
// BatchUpdateHeartbeats 批量更新 Ping 统计与活跃时间(单事务,心跳时间戳由 ConnectionStore 写 connect 表)
BatchUpdateHeartbeats(ctx context.Context, entries []*models.HeartbeatUpdateEntry) error
// BatchIncrementStats 批量递增消息/字节统计(单事务)
BatchIncrementStats(ctx context.Context, entries []*models.StatsIncrementEntry) error
// BatchAddErrors 批量记录连接错误(单事务)
// 同一连接的多次错误由调用方在攒批 flush 时合并为一条 ErrorUpdateEntry(count 累加、保留最新错误)
BatchAddErrors(ctx context.Context, entries []*models.ErrorUpdateEntry) error
// BatchFinalizeOnDisconnect 批量断开终评(唯一入口,单条 FinalizeOnDisconnect 已废弃)
// 停机路径逐连接终评 = 每连接 3 条 SQL(SELECT quality + SELECT duration + UPDATE score),
// 26w 连接 = 78w 条 SQL 在停机窗口直打 DB,是滚动更新期 DB 过载的主源;
// 批量化后 3 SQL/连接 → 每块 1 条 IN 读 + 1 条 CASE WHEN 写(Go 内算分),
// duration 由调用方从 MarkDisconnectedBatch 返回的 entries 带入(与 SQL 写入值同源,无需读回)
BatchFinalizeOnDisconnect(ctx context.Context, entries []*models.DisconnectionEntry) error
// GetByConnectionID 根据连接ID获取质量记录
GetByConnectionID(ctx context.Context, connectionID string) (*models.ConnectionQuality, error)
// GetByUserID 根据用户ID获取所有质量记录
GetByUserID(ctx context.Context, userID string) ([]*models.ConnectionQuality, error)
// GetHighErrorRateConnections 获取高错误率连接
GetHighErrorRateConnections(ctx context.Context, errorThreshold int, limit int) ([]*models.ConnectionQuality, error)
// GetFrequentReconnectConnections 获取频繁重连的连接
GetFrequentReconnectConnections(ctx context.Context, reconnectThreshold int, limit int) ([]*models.ConnectionQuality, error)
// Close 关闭仓库
Close() error
}
ConnectionQualityStore 连接质量存储接口 承载运行时质量指标(心跳/消息/错误/重连)与评分,随 batcher 高频批量更新
type ConnectionStore ¶
type ConnectionStore interface {
// Upsert 创建或更新连接记录(首次连接创建,重连时更新)
Upsert(ctx context.Context, record *models.ConnectionRecord) error
// BatchMarkDisconnected 批量标记断连终态(CASE WHEN 单 SQL 合并)
// 断连终态唯一写入口:正常注销经 DisconnectionBatcher 攒批 flush,停机路径分块直调;
// 写 duration/disconnected_at/is_abnormal 供质量终评(FinalizeOnDisconnect)读,
// connectedAt 由调用方从内存 Client 快照带入(省前置 SELECT);
// IN 影响 0 行(记录已被清理)静默返回;批内同连接去重保留最后(终态以最后一次为准)
BatchMarkDisconnected(ctx context.Context, entries []*models.DisconnectionEntry) error
// BatchUpdateHeartbeats 批量更新心跳时间戳(connect 表 last_ping_at,单 SQL)
BatchUpdateHeartbeats(ctx context.Context, entries []*models.HeartbeatUpdateEntry) error
// GetByConnectionID 根据连接ID获取连接记录
GetByConnectionID(ctx context.Context, connectionID string) (*models.ConnectionRecord, error)
// GetByUserID 根据用户ID获取所有连接记录(支持多设备)
GetByUserID(ctx context.Context, userID string) ([]*models.ConnectionRecord, error)
// GetActiveByUserID 根据用户ID获取所有活跃连接记录
GetActiveByUserID(ctx context.Context, userID string) ([]*models.ConnectionRecord, error)
// List 通用列表查询(支持条件过滤)
List(ctx context.Context, opts *models.ConnectionQueryOptions) ([]*models.ConnectionRecord, error)
// Count 统计连接数(支持条件过滤)
Count(ctx context.Context, opts *models.ConnectionQueryOptions) (int64, error)
// GetConnectionStats 获取连接统计信息(仅 connect 身份维度,质量维度由 ConnectionQualityStore 补充)
GetConnectionStats(ctx context.Context, startTime, endTime time.Time) (*models.ConnectionStats, error)
// GetConnectionStatsByID 根据连接ID获取单个连接的统计信息
GetConnectionStatsByID(ctx context.Context, connectionID string) (*models.UserConnectionStats, error)
// GetUserConnectionStats 获取用户所有连接的汇总统计
GetUserConnectionStats(ctx context.Context, userID string) (*models.UserConnectionStats, error)
// GetNodeConnectionStats 获取节点连接统计
GetNodeConnectionStats(ctx context.Context, nodeID string) (*models.NodeConnectionStats, error)
// BatchUpsert 批量创建或更新连接记录
BatchUpsert(ctx context.Context, records []*models.ConnectionRecord) error
// CleanupInactiveRecords 清理非活跃记录
CleanupInactiveRecords(ctx context.Context, before time.Time) (int64, error)
// Close 关闭仓库,停止后台任务
Close() error
}
ConnectionStore 连接记录存储接口
type DeliverEnvelope ¶
type DeliverEnvelope struct {
Intent DeliverIntent `json:"intent"` // 投递意图,空值消费端按路由字段推导
Publisher string `json:"publisher"` // 发布者标识(审计与 ACL 主体,必填)
PublishedAt time.Time `json:"published_at"` // 发布时刻(消费端时延监控)
ExcludeSender bool `json:"exclude_sender,omitempty"` // 广播排除发送者连接
Hops uint8 `json:"hops,omitempty"` // 转发跳数防环
Message *models.HubMessage `json:"message"`
}
DeliverEnvelope 投递信封 - MQ 传输的外层包装
路由维度复用 HubMessage 自带字段(AppID/Namespace/GroupIDs/Receiver), 信封只补 ctx 侧参数、溯源与转发防环:
- MessageID 必须非空:Go 发布者由 publisher 自动生成,异构发布者空值直接拒绝 (MessageID 是幂等去重键的一半,空值即去重失效)
- Hops 转发跳数防环:消费端本地 miss 转投定向 topic 时 +1,超限按等级兜底 (必达走离线库 / 可靠记失败态 / express 丢弃)
type DeliverIntent ¶
type DeliverIntent uint8
DeliverIntent 投递意图(决定 topic 路由与消费语义)
const ( // IntentUnset 未设置(零值):消费端按 Message 路由字段(GroupIDs/Namespace)推导 IntentUnset DeliverIntent = iota // IntentDeliver P2P / 群组投递(queue 语义消费) IntentDeliver // IntentBroadcast 全局/命名空间广播(fanout 语义消费) IntentBroadcast )
type EventBus ¶
type EventBus interface {
// Publish 发布投递指令,同步返回表示 MQ 已接收(At-Least-Once 前提)
// topic 路由由信封 Intent + Message 投递等级自动决定
Publish(ctx context.Context, env *DeliverEnvelope) error
// Subscribe 订阅投递指令,handler 返回 error 时按实现的 NACK 语义重投
// queue 语义(TopicDeliver)与 fanout 语义(TopicBroadcast)由 topic 参数区分
Subscribe(ctx context.Context, topic EventBusTopic, handler EventHandler) error
// Close 优雅关闭:排空在途消息、退出订阅循环
Close() error
}
EventBus 事件总线契约 - 下行投递指令的外部消息通道
语义分级(NATS 落地形态,其他适配器等价映射):
- express(IsExpress)→ core subject:at-most-once,~0.3ms
- reliable / 必达 → JetStream 分片:持久化 + 重投,分片内串行保序
- broadcast → core fanout:每节点一份,只投本地连接
type EventBusTopic ¶
type EventBusTopic string
EventBusTopic 订阅语义(Publish 侧由信封 Intent 自动路由,无需指定)
const ( // TopicDeliver 投递语义:queue 消费(consumer group 负载均衡,任一节点消费) TopicDeliver EventBusTopic = "deliver" // TopicBroadcast 广播语义:fanout 消费(每节点独立消费一份,只投本地连接) TopicBroadcast EventBusTopic = "broadcast" )
type EventHandler ¶
type EventHandler func(ctx context.Context, env *DeliverEnvelope) error
EventHandler 投递指令消费回调
返回 nil 表示消费成功(适配器侧 ack);返回 error 触发 NACK 重投 (At-Least-Once 下消费方必须幂等,broker 域按投递等级分流去重)
type GroupStore ¶
type GroupStore interface {
// CreateGroup 创建群组(存储元信息,若已存在则覆盖)
// group.AppID/Namespace 为空时自动填充默认值
CreateGroup(ctx context.Context, group *models.Group) error
// GetGroup 获取群组元信息
GetGroup(ctx context.Context, appID, namespace, groupID string) (*models.Group, error)
// DisbandGroup 解散群组(删除元信息、成员集合、命名空间索引及各成员的反向索引)
DisbandGroup(ctx context.Context, appID, namespace, groupID string) error
// AddMembers 添加成员到群组(同时更新成员的反向索引与命名空间索引)
AddMembers(ctx context.Context, appID, namespace, groupID string, userIDs []string) error
// RemoveMembers 从群组移除成员(同时清理成员的反向索引)
RemoveMembers(ctx context.Context, appID, namespace, groupID string, userIDs []string) error
// GetMembers 获取群组所有成员ID
GetMembers(ctx context.Context, appID, namespace, groupID string) ([]string, error)
// GetUserGroups 获取用户在指定 (appID, namespace) 下加入的所有群组ID
GetUserGroups(ctx context.Context, appID, namespace, userID string) ([]string, error)
// IsMember 判断用户是否为群组成员
IsMember(ctx context.Context, appID, namespace, groupID, userID string) (bool, error)
// GetMemberCount 获取群组成员数量
GetMemberCount(ctx context.Context, appID, namespace, groupID string) (int64, error)
// GetNamespaceGroups 获取 (appID, namespace) 下所有群组ID
GetNamespaceGroups(ctx context.Context, appID, namespace string) ([]string, error)
// GetAllNamespaces 获取指定 appID 下所有有群组的命名空间ID(用于该 app 的全命名空间广播)
GetAllNamespaces(ctx context.Context, appID string) ([]string, error)
// GetMultiGroupMembers 批量获取多个群组的成员(跨 namespace 聚合,两段 Pipeline 共 2 次网络往返)
// 同一 groupID 可在多个 namespace 下各建实例(业务侧按租户建组),本方法按 (appID, groupIDs)
// 跨 ns 聚合所有实例的成员并合并去重;返回 map[groupID][]memberIDs,无实例的 gid 该 key 缺失
GetMultiGroupMembers(ctx context.Context, appID string, groupIDs []string) (map[string][]string, error)
// EnsureSystemGroup 确保系统保留组存在(__ 前缀,agent/observer 自动加入前初始化)
// 幂等:不存在则创建,已存在则返回 nil
EnsureSystemGroup(ctx context.Context, appID, namespace, groupID string) error
// SetInvalidateNotifier 注入拓扑写路径失效回调(拓扑写本地逐出后触发,供跨节点失效广播聚合)
// 持本地缓存的实现(GroupMemberCache)存回调并在写路径统一触发;无本地缓存的裸仓储 no-op 丢弃
SetInvalidateNotifier(fn func(appID, groupID string))
// InvalidateTopology 逐出该群组的本地缓存拓扑(跨节点失效广播的消费入口)
// 纯逐出不写负缓存:失效原因是拓扑变更(含建组/增成员),写负缓存会把新实例误报为无实例;
// 无本地缓存的裸仓储 no-op
InvalidateTopology(appID, groupID string)
}
GroupStore 群组存储接口
type HubStats ¶
type HubStats interface {
// UpdateConnectionStats 批量更新连接统计(总连接数+1、活跃连接数、心跳时间)
UpdateConnectionStats(ctx context.Context, nodeID string, activeCount int64) error
// IncrementTotalConnections 增加总连接数
IncrementTotalConnections(ctx context.Context, nodeID string, delta int64) error
// SetActiveConnections 设置当前活跃连接数
SetActiveConnections(ctx context.Context, nodeID string, count int64) error
// IncrementMessagesSent 增加已发送消息数
IncrementMessagesSent(ctx context.Context, nodeID string, delta int64) error
// IncrementMessagesReceived 增加已接收消息数
IncrementMessagesReceived(ctx context.Context, nodeID string, delta int64) error
// IncrementBroadcastsSent 增加已发送广播数
IncrementBroadcastsSent(ctx context.Context, nodeID string, delta int64) error
// RegisterNode 注册节点并初始化统计信息(设置启动时间、添加到节点集合)
RegisterNode(ctx context.Context, nodeID string, startTime int64) error
// GetNodeStats 获取指定节点的统计信息
GetNodeStats(ctx context.Context, nodeID string) (*models.NodeStats, error)
// GetAllNodesStats 获取所有节点的统计信息
GetAllNodesStats(ctx context.Context) (map[string]*models.NodeStats, error)
// GetTotalStats 获取集群总统计信息(所有节点汇总)
GetTotalStats(ctx context.Context) (*models.ClusterStats, error)
// CleanupNodeStats 清理已下线节点的统计数据
CleanupNodeStats(ctx context.Context, nodeID string) error
// UpdateNodeHeartbeat 更新节点心跳时间
UpdateNodeHeartbeat(ctx context.Context, nodeID string) error
// GetActiveNodes 获取活跃的节点列表(基于心跳)
GetActiveNodes(ctx context.Context, timeout time.Duration) ([]string, error)
}
HubStats Hub 统计信息接口
职责:
- 节点级统计:连接数、消息收发、广播计数
- 集群级统计:所有节点汇总
- 节点注册发现:注册/注销、心跳、TTL 管理
存储维度:
- 按 nodeID 隔离,key = "{prefix}node:{nodeID}"
- 全局节点集合 key = "{prefix}nodes"
type Logger ¶
Logger 日志接口,等价于 go-logger.ILogger
func InitLogger ¶
InitLogger 根据配置初始化日志器 Logging 未启用时回退 NewDefaultLogger,启用时使用配置构造的 go-logger 实例
func NewDefaultLogger ¶
func NewDefaultLogger() Logger
NewDefaultLogger 返回默认日志器(进程内单例)
输出经 logger.NewConsoleWriter 批量合并,而非直写 os.Stdout: 直写形态下 go-logger 的 isConcurrentSafeOutput 对 os.Stdout 判定为 false (其并发安全未受保证),outLockFree 保持 false,于是每行一次互斥 + 一次 write(2)。 consoleLogWriter 实现 concurrentSafeMarker,WithOutput 会自动把 outLockFree 置为 true,互斥与逐行 syscall 一并消除。
实测(go-wsc 默认配置,真实文件,见 logger_bench_test.go): 串行 1280-1460ns → 215-221ns(约 6.0×),并行 1634-1890ns → 303-381ns(约 5.2×)。
⚠️ 缓冲意味着进程退出前必须刷出,否则丢失末尾日志。Hub 的 SafeShutdown 已挂 FlushDefaultLogger;不经 Hub 生命周期的调用方(如独立使用 adapter)须自行调用。
type MessageQueue ¶
type MessageQueue interface {
// Enqueue 入队消息
Enqueue(ctx context.Context, queueName string, msg *models.HubMessage) error
// Dequeue 出队消息(阻塞式,带看门狗锁保证消息不会因消费者崩溃而丢失)
// timeout 为最长阻塞等待时间,超时返回 (nil, nil)
Dequeue(ctx context.Context, queueName string, timeout time.Duration) (*models.HubMessage, error)
// DequeueBatch 批量出队(非阻塞,单次往返)
// 返回条数不足 count 表示队列已空
DequeueBatch(ctx context.Context, queueName string, count int) ([]*models.HubMessage, error)
// GetLength 获取队列当前长度
GetLength(ctx context.Context, queueName string) (int64, error)
// Clear 清空队列
Clear(ctx context.Context, queueName string) error
// Peek 查看队列头部消息(不移除)
Peek(ctx context.Context, queueName string) (*models.HubMessage, error)
}
MessageQueue 通用消息队列契约
消费者按 queueName 分区,各分区互不干扰实现需保证同一 queueName 内 先进先出;跨 queueName 不保证顺序
type MessageRole ¶
type MessageRole string
MessageRole 查询视角 —— 用户是消息的接收者还是发送者
const ( // MessageRoleReceiver 作为接收者查询 MessageRoleReceiver MessageRole = "receiver" // MessageRoleSender 作为发送者查询 MessageRoleSender MessageRole = "sender" )
type MessageSink ¶
type MessageSink interface {
// CreateBatch 批量创建消息发送记录(outbox 攒批 flush 用,一条 SQL 写整批;
CreateBatch(ctx context.Context, records []*models.MessageSendRecord) error
// FindByID 根据自增ID查找
FindByID(ctx context.Context, id uint) (*models.MessageSendRecord, error)
// FindByMessageID 根据消息ID+接收者复合键查找(P2P 同一 message_id 为每个 receiver 各建一条记录)
FindByMessageID(ctx context.Context, key models.MessageRecordKey) (*models.MessageSendRecord, error)
// QueryRecords 查询消息记录(支持按状态、发送者、接收者、节点IP、客户端IP等条件过滤)
QueryRecords(ctx context.Context, filter *models.MessageRecordFilter) ([]*models.MessageSendRecord, error)
// FindRetryable 查找可重试的记录
FindRetryable(ctx context.Context, limit int) ([]*models.MessageSendRecord, error)
// DeleteExpired 删除过期的记录
DeleteExpired(ctx context.Context) (int64, error)
// Delete 删除记录
Delete(ctx context.Context, id uint) error
// DeleteByMessageID 根据消息ID删除
DeleteByMessageID(ctx context.Context, messageID string) error
// BatchUpdateStatus 批量更新消息状态(按复合键批量定位;相同 status/reason/errorMsg 用一条 SQL;
// 状态更新唯一路径,经 MessageStatusUpdater 攒批调用,单条 UpdateStatus 已移除)
BatchUpdateStatus(ctx context.Context, keys []models.MessageRecordKey, status models.MessageSendStatus, reason models.FailureReason, errorMsg string) error
// ClaimStaleSending 原子认领超时的 sending 记录(仅当状态仍为 sending 时按复合键更新)
// 返回实际认领成功的复合键列表(状态已被并发方更新则不认领)
ClaimStaleSending(ctx context.Context, keys []models.MessageRecordKey, newStatus models.MessageSendStatus, reason models.FailureReason, errorMsg string) ([]models.MessageRecordKey, error)
// IncrementRetry 增加重试次数(按复合键定位单条记录)
IncrementRetry(ctx context.Context, key models.MessageRecordKey, attempt models.RetryAttempt) error
// GetStatistics 获取统计信息
GetStatistics(ctx context.Context) (map[string]int64, error)
// CleanupOld 清理旧记录
CleanupOld(ctx context.Context, before time.Time) (int64, error)
// Close 关闭仓库,停止后台任务
Close() error
}
MessageSink 消息归档与统计接口
职责:
- MessageRecord:消息发送记录的 CRUD + 批量更新 + 统计
- HubStats:节点/集群级别的连接与消息统计
存储维度:
- MessageRecord:按 message_id(全局唯一)主键,支持按 sender/receiver/node_ip/client_ip/status 过滤
- HubStats:按 nodeID 隔离,key = "{prefix}node:{nodeID}",全局汇总 key = "{prefix}nodes"
type OfflineMessageFilter ¶
type OfflineMessageFilter struct {
// UserID 用户ID
UserID string
// Role 查询视角(接收者/发送者)
Role MessageRole
// AppID 应用ID(空表示查询所有应用)
AppID string
// Namespace 命名空间ID(空表示所有命名空间)
Namespace string
// GroupID 群组ID(空表示非群组消息或所有消息)
GroupID string
// Limit 数量限制
Limit int
// Cursor 分页游标(message_id)
Cursor string
// Statuses 消息状态列表,为空则待处理状态
Statuses []models.MessageSendStatus
// ExcludeExhausted 排除重试耗尽的消息(retry_count >= max_retry,毒消息死信)
// 推送失败由 UpdatePushStatus 原子递增 retry_count,超限后不再进入推送流程
// max_retry <= 0 视为不限制
ExcludeExhausted bool
}
OfflineMessageFilter 离线消息查询条件
type OfflineQueue ¶
type OfflineQueue interface {
// StoreOfflineMessage 存储离线消息(双写 Redis + MySQL)
// 落入 ctx 的 (ns, group) 对应的分区:Redis key = ns:group:userID
StoreOfflineMessage(ctx context.Context, userID string, msg *models.HubMessage) error
// DrainOfflineQueue 排空 Redis 队列(单组 FIFO,仅 Redis,不触及 MySQL)
// 按 ctx 的单个 (ns, group) 出队,limit<=0 表示一次取尽
// 调用方按组循环调用(hub replay 枚举用户所有 group + P2P)
DrainOfflineQueue(ctx context.Context, userID string, limit int) ([]*models.HubMessage, error)
// GetOfflineMessages 查询 MySQL 离线消息(跨组,命名空间内该用户全部 group)
// limit: >0 最多返回指定数量;<=0 最多 1 万条
// cursor: 上次返回的最后一条 message_id,空串表示从头
// 返回 nextCursor 为空表示无更多数据
GetOfflineMessages(ctx context.Context, userID string, limit int, cursor string) ([]*models.HubMessage, string, error)
// DeleteOfflineMessages 按消息ID删除已推送的离线消息(MySQL,跨组按 message_id 删)
DeleteOfflineMessages(ctx context.Context, userID string, messageIDs []string) error
// GetOfflineMessageCount 获取离线消息数量(MySQL 跨组计数,MySQL 为双写超集故计数准确)
GetOfflineMessageCount(ctx context.Context, userID string) (int64, error)
// ClearOfflineMessages 清空用户的离线消息
// groupIDs: 用户在该命名空间下的全部 group + "" (P2P),逐组清 Redis + 一次清 MySQL
ClearOfflineMessages(ctx context.Context, userID string, groupIDs []string) error
// UpdatePushStatus 更新离线消息推送状态
// pushErr: 推送结果错误,nil表示成功,非nil表示失败
UpdatePushStatus(ctx context.Context, messageIDs []string, pushErr error) error
}
OfflineQueue 离线消息处理器接口(业务逻辑层)
存储维度:按 (appID, namespace, groupID, userID) 四元组隔离
- Redis 队列 key = "{prefix}{appID}:{ns}:{groupID}:{userID}"(P2P 消息 groupID 为空)
- MySQL 记录带 app_id + namespace + group_id + receiver 列
appID 是最上层隔离维度(默认 "__default_app__"),namespace + groupID 从 ctx 路由元数据提取 (hub 层 routing.NewRoute().WithAppID(...).Inject(ctx) 注入)appID 严格隔离,跨 app 不串扰
type OfflineStore ¶
type OfflineStore interface {
// Save 保存离线消息
Save(ctx context.Context, record *models.OfflineMessageRecord) error
// BatchSave 批量保存离线消息
BatchSave(ctx context.Context, records []*models.OfflineMessageRecord) error
// QueryMessages 查询离线消息(支持按接收者/发送者、分页、状态过滤)
QueryMessages(ctx context.Context, filter *OfflineMessageFilter) ([]*models.OfflineMessageRecord, error)
// DeleteByMessageIDs 批量删除离线消息(按应用+命名空间+接收者)
DeleteByMessageIDs(ctx context.Context, appID, namespace, receiverID string, messageIDs []string) error
// GetCountByReceiver 获取用户作为接收者的离线消息数量
GetCountByReceiver(ctx context.Context, appID, namespace, receiverID string) (int64, error)
// GetCountBySender 获取用户作为发送者的离线消息数量
GetCountBySender(ctx context.Context, appID, namespace, senderID string) (int64, error)
// ClearByReceiver 清空用户作为接收者的所有离线消息
ClearByReceiver(ctx context.Context, appID, namespace, receiverID string) error
// DeleteExpired 删除过期的离线消息
DeleteExpired(ctx context.Context) (int64, error)
// UpdatePushStatus 更新离线消息推送状态
// status: 消息状态(pending/success/failed)
// errorMsg: 错误信息(失败时)
UpdatePushStatus(ctx context.Context, messageIDs []string, status models.MessageSendStatus, errorMsg string) error
// CleanupOld 清理 before 之前的旧记录
CleanupOld(ctx context.Context, before time.Time) (int64, error)
// Close 关闭仓库,停止后台任务
Close() error
}
OfflineStore 离线消息的 RDBMS 持久化契约
所有查询/删除均按 (appID, namespace, userID) 三元组隔离; appID 是最上层隔离维度,跨 app 绝不串扰
type OnlineStore ¶
type OnlineStore interface {
// SetClientOnline 设置客户端在线(支持多设备)
SetClientOnline(ctx context.Context, client *models.Client) error
// SetClientOffline 设置指定客户端离线
SetClientOffline(ctx context.Context, client *models.Client) error
// SetOffline 设置用户所有客户端离线
SetOffline(ctx context.Context, userID string) error
// GetClient 获取客户端信息
GetClient(ctx context.Context, clientID string) (*models.Client, error)
// GetClientOwner 获取 clientID 当前归属节点(上线脚本写入 owner key)
// 返回空串表示无归属记录(旧数据或已过期);用于检测同 clientID 跨节点迁移
GetClientOwner(ctx context.Context, clientID string) (string, error)
// GetUserClients 获取用户的所有在线客户端
GetUserClients(ctx context.Context, userID string) ([]*models.Client, error)
// UpdateClientHeartbeat 更新客户端心跳
UpdateClientHeartbeat(ctx context.Context, clientID string) error
// IsUserOnline 检查用户是否在线(任意设备,按 ctx 路由信封 appID+namespace 隔离)
// 路径自适应:有路由信封时走 L1 offset 缓存 → GETBIT(命中零网络),
// 无路由信封走 unscoped GETBIT;位即真相,无 ZSET 兜底
IsUserOnline(ctx context.Context, userID string) (bool, error)
// BatchIsUserOnline 批量在线判定,返回 map[userID]bool(含全部查询的 userID)
// Pipeline 实现:offset 解析(L1+HGET)→ GETBIT,两次往返覆盖 N 个用户(全命中 L1 时一次)
BatchIsUserOnline(ctx context.Context, userIDs []string) (map[string]bool, error)
// GetAllOnlineUsers 获取所有在线用户ID列表
GetAllOnlineUsers(ctx context.Context) ([]string, error)
// GetOnlineCount 获取在线用户总数
GetOnlineCount(ctx context.Context) (int64, error)
// GetOnlineUsersByType 根据用户类型获取在线用户
GetOnlineUsersByType(ctx context.Context, userType models.UserType) ([]string, error)
// GetUserNodes 获取用户所在的所有节点(支持多设备)
GetUserNodes(ctx context.Context, userID string) ([]string, error)
// BatchGetUserNodes 批量获取多个用户所在的所有节点(Pipeline 优化,避免 N+1 查询)
// 返回 map[userID][]nodeIDs,未找到的 userID 不在 map 中
BatchGetUserNodes(ctx context.Context, userIDs []string) (map[string][]string, error)
// GetNodeClients 获取节点的所有在线客户端
GetNodeClients(ctx context.Context, nodeID string) ([]*models.Client, error)
// GetNodeUsers 获取节点的所有在线用户ID
GetNodeUsers(ctx context.Context, nodeID string) ([]string, error)
// BatchSetClientsOnline 批量设置客户端在线(全量:JSON 序列化 + SETEX client 详情)
BatchSetClientsOnline(ctx context.Context, clients []*models.Client) error
// RenewClientsOnline 心跳批量续期(轻量路径:跳过序列化/压缩/SETEX,仅续期索引与 bitmap)
// client:<id> 键缺失(过期/淘汰)的客户端内部自动走全量重建,自愈语义与 BatchSetClientsOnline 一致
// 供心跳高频刷新使用,千万级连接下避免每周期全量重写 client 详情
RenewClientsOnline(ctx context.Context, clients []*models.Client) error
// BatchSetClientsOffline 批量设置客户端离线
BatchSetClientsOffline(ctx context.Context, clientIDs []string) error
// BatchSetClientsOfflineWithInfo 批量设置客户端离线(使用已知的客户端信息)
// 客户端信息已知时使用,避免从 Redis 查询,确保即使 client key 已被删除也能清理 bitmap 位与索引
BatchSetClientsOfflineWithInfo(ctx context.Context, clients []*models.Client) error
// CleanupExpired 清理当前节点的过期客户端
CleanupExpired(ctx context.Context, nodeID string) (int64, error)
}
OnlineStore 在线状态仓库接口
type RouteAdvisor ¶
type RouteAdvisor interface {
// NodeOf 返回用户连接所在节点;空列表 = 离线/未知(投共享 topic 或直接离线)
NodeOf(ctx context.Context, appID, namespace, userID string) []string
}
RouteAdvisor 路由查询端口 - 发布侧粘性定向(L0)的依据
实现:redisadapter 基于在线索引(GetUserNodes)+ 本地短 TTL 缓存; 未注入时 publisher 自动降级投共享通道,行为不变——粘性是性能优化 不是正确性前提
type StoreDeps ¶
type StoreDeps struct {
// Redis Redis 客户端(纯 RDBMS 部署时为 nil)
Redis redis.UniversalClient
// DB GORM 实例(纯 Redis 部署时为 nil)
DB *gorm.DB
}
StoreDeps 装配依赖 —— 一次交给适配器,由适配器各取所需
不拆成「Redis 一份、RDBMS 一份」两个方法:离线消息处理器这类组件 同时需要队列(Redis)与持久化(RDBMS),拆开后任何单个 hook 都拿不到 完整依赖,只能靠调用方在外部手工补齐,反而把装配逻辑漏回业务侧
type StoreHooks ¶
type StoreHooks interface {
// Configure 用给定依赖装配该适配器负责的全部仓库
//
// 返回:
// - error: 仅在该适配器所需依赖齐备却构造失败时返回;
// 依赖缺失(如纯 Redis 场景无 DB)应直接跳过并返回 nil
Configure(ctx context.Context, hub StoreTarget, deps StoreDeps) error
}
StoreHooks 存储层装配钩子
每个适配器包提供一个实现,用自己的依赖构造仓库实现并注入 Hub Hub 侧只认本接口,不认识任何具体适配器类型
依赖缺失时应跳过而非报错:纯 Redis 部署下 gorm 适配器的 hook 只要 检测到 deps.DB == nil 就返回 nil,不影响另一侧装配
type StoreTarget ¶
type StoreTarget interface {
SetOnlineStatusRepository(store OnlineStore)
SetHubStatsRepository(store HubStats)
SetGroupRepository(store GroupStore)
SetWorkloadRepository(store WorkloadStore)
SetMessageRecordRepository(sink MessageSink)
SetConnectionRecordRepository(store ConnectionStore)
SetConnectionQualityRepository(store ConnectionQualityStore)
SetOfflineMessageHandler(queue OfflineQueue)
}
StoreTarget 装配目标 —— 适配器通过这些 setter 把实现交给 Hub
能力面按「适配器实际需要注入哪些仓库」裁剪,而非抄 Hub 的完整 setter 表
type WorkloadStore ¶
type WorkloadStore interface {
// ReloadAgentWorkload 重新加载客服工作负载(客服上线时调用)
// 优先级:Redis > DB > 默认值 0;从持久化存储恢复负载,不存在则初始化为 0 并同步 ZSet
ReloadAgentWorkload(ctx context.Context, agentID string) (int64, error)
// ForceSetAgentWorkload 强制设置客服工作负载(慎用,会覆盖现有值)
// 直接覆盖 Redis 和 ZSet 中的负载值,异步同步到 DB
ForceSetAgentWorkload(ctx context.Context, agentID string, workload int64) error
// GetAgentWorkload 获取客服工作负载
GetAgentWorkload(ctx context.Context, agentID string) (int64, error)
// IncrementAgentWorkload 增加客服工作负载
IncrementAgentWorkload(ctx context.Context, agentID string) error
// DecrementAgentWorkload 减少客服工作负载
DecrementAgentWorkload(ctx context.Context, agentID string) error
// GetLeastLoadedAgent 获取负载最小的在线客服
GetLeastLoadedAgent(ctx context.Context, onlineAgents []string, dimension models.WorkloadDimension) (string, int64, error)
// AcquireLeastLoadedAgent 原子地选择负载最小的在线客服并将其负载 +1
//
// 与 GetLeastLoadedAgent 的区别:
// - GetLeastLoadedAgent 只读,纯查询,返回后业务侧还需单独调用 IncrementAgentWorkload,
// 这会导致"读-改-写"之间存在 TOCTOU 窗口,多进程/多 goroutine 并发时读到同一份旧负载快照,
// 选出同一个客服,造成工单扎堆分配到同一人(负载均衡不均)
// - AcquireLeastLoadedAgent 在同一段 Lua 中完成 "选中 + 所有维度原子 +1",
// Redis 单线程保证跨进程原子,是分布式场景下负载均衡分配的正确做法
//
// 返回值:
// - agentID: 被选中的客服 ID
// - workload: 选中客服在选中前的 realtime 负载值(用于日志/监控)
// - error: 执行失败的错误
//
// 业务失败回滚:调用方应在后续业务失败时调用 DecrementAgentWorkload 回滚此次预扣减
AcquireLeastLoadedAgent(ctx context.Context, onlineAgents []string, dimension models.WorkloadDimension) (string, int64, error)
// RemoveAgentWorkload 移除客服负载记录(客服下线时调用)
// 只删除 ZSet 记录,保留 string key 以便重新上线时恢复
RemoveAgentWorkload(ctx context.Context, agentID string) error
// GetAllAgentWorkloads 获取所有客服负载(按负载升序)
GetAllAgentWorkloads(ctx context.Context, limit int64) ([]models.WorkloadInfo, error)
}
WorkloadStore 客服负载存储接口