轮询调度不能直接用 roundrobin 包,因其不感知客服在线、忙闲等业务状态;需用 sync.RWMutex 保护在线客服列表,atomic 原子操作轮询索引,过滤 Online && !Busy 后取模,并立即标记 Busy + 超时释放保障一致性。

轮询调度为什么不能直接用 roundrobin 包?
Go 生态里确实有 roundrobin 这类第三方包,但它们通常面向底层连接池或 RPC 负载均衡,不感知客服状态(比如是否在线、是否正忙、是否已掉线)。Gin 是 HTTP 框架,你真正要调度的是「客服人员」这个业务实体,不是 TCP 连接。直接套用会漏掉关键逻辑:客服掉线后没及时剔除、并发请求下状态竞争、没考虑同一用户连续会话需绑定同一客服等。
如何用 Gin 管理客服状态并做线程安全轮询?
核心是维护一个可变的在线客服列表,并保证多请求并发访问时读写安全。不要用全局 slice + sync.Mutex 粗暴锁整个列表——轮询本身是只读操作,只有上线/下线才写,应该读写分离。
- 用
sync.RWMutex保护客服列表,读多写少场景下性能更好 - 客服结构体至少包含:
ID、Name、Online(bool)、Busy(bool)、LastActive(time.Time) - 轮询索引用原子变量
sync/atomic,避免加锁:声明var nextIndex int64,每次atomic.AddInt64(&nextIndex, 1)后取模 - 轮询前先过滤出
Online && !Busy的客服,再对过滤后列表取模,否则会跳过大量无效项导致倾斜
Gin 路由中怎么嵌入轮询逻辑?
别在每个 handler 里重复写轮询代码。封装成中间件或独立服务更清晰。推荐定义一个 GetNextAgent() 函数,返回 *Agent 或 nil(无可用客服),并在 handler 中显式处理 nil 情况(比如返回 503 或排队提示)。
func assignAgent(c *gin.Context) {
agent := GetNextAgent()
if agent == nil {
c.JSON(503, gin.H{"error": "no available agent"})
return
}
// 绑定客服到当前会话(比如存入 Redis 或写入响应 header)
c.Header("X-Assigned-Agent-ID", strconv.Itoa(agent.ID))
c.Next()
}
注意:这里只是分配,真正的「绑定」需要后续消息路由逻辑配合(比如 WebSocket 消息按 sessionID 转发到对应客服),Gin 本身不维持长连接状态。
为什么轮询后要立即标记客服为 Busy?
否则高并发下多个请求可能同时拿到同一个客服,造成重复分配。必须在确认分配成功后立刻更新其 Busy 状态,并设置超时自动释放(比如 30 秒无心跳则置 Busy=false)。
- 更新状态建议走单独 goroutine + 延迟执行,避免阻塞 HTTP 响应
- 用
time.AfterFunc或简单启动go func() { time.Sleep(30*time.Second); setFree(agent.ID) }() - 更健壮的做法是结合心跳机制:客服端定时上报,服务端维护
LastHeartbeat,轮询时只选LastHeartbeat > now-10s的人
轮询本身很简单,难的是状态一致性。客服上线、掉线、忙线、超时释放这些事件的时序和并发控制,才是实际落地中最容易出错的地方。


















