workerman 不提供客服系统核心逻辑,需自研调度层;坐席状态不能依赖 connection 对象判断,必须通过 redis 持久化+心跳机制管理;新访客分配应使用带权重的 zset + lua 原子操作实现;消息透传需结合 redis list 缓存与重连补发。

Workerman 本身不提供坐席状态管理、会话分配、消息路由等客服系统核心逻辑,必须自己实现调度层;直接用它做多坐席分流,容易陷入连接管理正确性、状态一致性、负载误判等坑。
为什么不能直接靠 Worker 或 Connection 对象判断坐席在线状态
Workerman 的 Connection 是长连接句柄,只反映 TCP 层是否存活,不等于坐席“可用”——比如坐席网页卡死、网络抖动、主动挂起但连接未断,$connection->isConnected() 仍返回 true。依赖这个做分流,会导致消息发给无响应坐席,用户等待超时。
- 必须引入独立的心跳机制(如每 15 秒客户端主动上报
ping,服务端记录last_heartbeat_time) - 坐席状态要持久化到 Redis,字段至少包含:
seat_id、status(online/away/offline)、last_heartbeat_time、current_queue_length - 所有坐席上线/下线/切换状态,必须原子更新 Redis,不能只改内存变量
怎么实现「新访客自动分配给最空闲坐席」的逻辑
不能用轮询或随机,也不能在每次分配时遍历全部坐席查 Redis——高并发下 Redis 成瓶颈,且无法实时反映排队压力。推荐用「带权重的 ZSET + 原子 pop」组合:
- 用 Redis ZSET 存坐席队列,score = 当前接待客户数 × 1000 + 时间戳(避免 score 相同导致顺序不确定)
- 新访客到来时,
ZRANGE seats:available 0 0 WITHSCORES拿出 score 最小的坐席(即当前队列最短的) - 紧接着用 Lua 脚本原子执行:检查该坐席当前状态是否为
online、INCR其队列长度、ZADD更新 score —— 三步不可拆分 - 如果 Lua 返回失败(比如状态已变),重试下一轮
ZRANGE,最多 3 次后降级为人工指定坐席
消息透传时如何避免坐席连接丢失导致消息堆积或丢失
Workerman 默认不缓存离线消息。坐席掉线瞬间,若访客正发送消息,$connection->send() 会失败,但框架不会自动重试或暂存。
- 发送前必须调用
$connection->isConnected(),且检查 Redis 中该坐席status是否为online - 若任一条件不满足,把消息写入 Redis List(如
msg:queue:seat_123),并设置过期时间 5 分钟 - 坐席重连成功后,触发
onConnect回调,立即从对应 ListLPOP最多 10 条历史消息补发 - 禁止用 PHP 内存数组暂存消息——进程重启就全丢,且多 Worker 进程间不共享
真正难的不是接住连接,而是让每个坐席的状态可观察、可干预、可回溯。Redis 的原子操作和 Lua 脚本不是可选项,是维持分流逻辑可信的底线。漏掉心跳校验或绕过 Redis 直接读内存状态,系统上线三天内就会出现“明明坐席在线却收不到消息”的故障。











