中介者必须封装状态与路由逻辑,成为唯一消息分发中心,维护用户索引、统一入口、禁止用户间直接引用,并依赖外部存储持久化状态,避免全量遍历实现高效广播。

中介者类必须封装所有聊天室状态和路由逻辑
直接让 User 对象互相调用发送消息,会迅速导致类之间网状依赖。中介者(比如 ChatRoomMediator)要成为唯一的消息分发中心,它持有所有注册用户的引用,并决定谁该收到哪条消息——不是靠用户自己遍历列表,而是由中介者统一调度。
常见错误是把中介者写成空壳,只转发消息却不管理状态:比如没记录在线用户、不校验发送权限、不处理离线消息缓存。结果还是得在 User::send() 里查用户列表、判状态、过滤接收人,等于白加一层。
实操建议:
-
ChatRoomMediator应维护一个$users数组(键为用户ID,值为User实例),提供registerUser()和unregisterUser() - 所有消息入口统一走
mediateMessage($senderId, $recipientId, $content),内部做权限检查、在线判断、格式预处理 - 避免在中介者里写业务规则(如“VIP用户可@所有人”),应抽成策略对象,通过依赖注入传入
用户类只能持有中介者引用,禁止持有其他用户引用
这是解耦成败的关键红线。如果 User 类里有 $this->friends 或 $this->roomUsers 这类数组,哪怕只是只读,也意味着它知道其他用户的存在,违反中介者本意。
典型翻车场景:前端发来“拉黑某人”,后端在 User::block($targetId) 里直接操作 $this->blockedIds[] 并调用 $target->onBlockedBy($this) —— 这个回调立刻重建了用户间强依赖。
正确做法是把“拉黑”也当作需中介协调的行为:
-
User::block($targetId)只调用$this->mediator->handleBlock($this->id, $targetId) -
ChatRoomMediator::handleBlock()更新内部黑名单映射表,并在后续mediateMessage()中自动过滤 - 被拉黑方不需要感知,也不需要任何回调——中介者静默拦截即可
注意 PHP 的对象生命周期与中介者单例陷阱
PHP 是无状态的请求级执行模型,ChatRoomMediator 不能简单用 static 单例跨请求共享用户列表——那会导致不同用户的会话数据混在一起,或者内存泄漏。
真正需要持久化的状态(如在线用户、未读消息)必须落地到外部存储。中介者本身应是轻量、无状态的协调器,每次请求实例化,但通过依赖注入获得 Redis 或数据库连接来读写真实状态。
实操建议:
- 不要用
self::$instance持有用户集合;改用Redis::hGetAll('chat:room:online')获取当前在线用户ID列表 - 中介者构造函数接收
RedisInterface $redis和MessageQueue $mq,而非自己 new 出来 - WebSocket 长连接场景下,中介者可绑定到连接生命周期(如 Swoole 的
Server::onMessage回调中 new 一个),但绝不跨连接复用
消息广播性能瓶颈往往出在中介者没做接收方预过滤
当群聊人数达几百时,foreach ($this->users as $user) 全量遍历再逐个判断是否在线/是否屏蔽/是否订阅该频道,CPU 就上去了。这不是设计问题,是漏掉了前置剪枝。
关键优化点在于:中介者必须根据消息类型,提前索引接收方。比如“全员公告”走 $this->getAllOnlineUsers(),“指定房间”走 $this->getUsersInRoom($roomId),“@某人”则只查 $this->getUserById($mentionedId)。
示例代码片段(非完整类):
// 在 ChatRoomMediator 中
public function broadcastToRoom(string $roomId, string $content): void
{
$userIds = $this->redis->smembers("room:{$roomId}:members"); // Redis Set 存成员ID
$onlineIds = array_intersect($userIds, $this->getOnlineUserIds()); // 取交集
foreach ($onlineIds as $uid) {
$this->deliverToUser($uid, $content);
}
}
这里 smembers 和 getOnlineUserIds() 都是 O(1) 或 O(n) 但 n 是实际在线数,不是全站用户数。漏掉这层,很容易在压测时发现 CPU 90% 耗在 array_filter 上。
复杂点在于房间关系可能是动态的(如临时频道、权限分级),所以索引结构得配合业务变——但无论如何,不能让中介者在每次发消息时从头算一遍谁该收。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











