Swoole WebSocket 消息服务系统设计:架构、存储与统计

拾柒
拾柒
发布时间:2026-10-15 | 4人浏览过
原创

本方案旨在为基于 Swoole 的 PHP WebSocket 服务提供高并发下的消息推送与计数解决方案。核心内容包括 Token 鉴权与防雪崩缓存策略、异步 HTTP 接入与 Redis 双队列缓冲、MySQL 分表异步存储,以及基于计数器与时间戳分离的用户未读消息统计模型。

系统概述

该 WebSocket 服务主要承担两大核心业务:面向客户端(Android/iOS)的用户消息推送服务,以及内部的客服 IM 消息系统。前端通过 Web 服务交互,后端依赖 Swoole 维持长连接。整体架构分为对外 WebSocket 长连接服务、对内 Http 回调服务以及底层数据存储层。

第1步:鉴权与缓存防雪崩策略

目的:确保请求合法性,同时防止因缓存集中过期导致的数据库雪崩效应。

入口:Swoole WebSocket OnOpen 事件或 Http 请求入口。

操作:

  1. 服务端接收到携带 Token 的请求,调用用户中心接口验证权限并获取 uid。
  2. 若验证通过,将 Token 与 UID 的关系存入 Redis。关键参数设置:为避免缓存同一时间过期,根据访问峰值(晚间 21-24 点,约 100w 请求/小时)计算随机偏移量。

原文明确指出,用户中心默认缓存时间为 7300s。为了防止缓存雪崩,本地服务的过期时间公式设计为基准时间加上随机值:

$uid = $redis->get($token);
// 基准3650秒 + 随机1~3000秒,确保总和小于7300s
$expireTime = 3650 + rand(1, 3000);
$uid = OAuth::getUserInfo($token);
if (!empty($uid) && intval($uid) > 0) {
    // 存储 Token 到 UID 的映射
    $redis->setEx($token, $expireTime, $uid);
}
if($uid && $uid > 0){
    // 存储 UID 到 Token 的反向映射
    $key = 'token_'.$uid;
    $redis->setEx($key, $expireTime, $token);
}

第2步:Http 服务接入与消息生产

目的:安全地接收主站业务变更(如评论、私信)产生的消息,解耦业务逻辑与消息推送。

条件:Http 服务仅允许云服务器内网访问,确保安全。

操作顺序:

  1. 异步埋点触发:主站发生系统消息、评论等操作时,异步发起 HTTP POST 请求至 Swoole 接口。设置超时时间为 2 秒。
  2. 重试机制:若首次请求失败(返回 false),休眠 100ms 后进行重试。由于是局域网传输,失败率极低,通常意味着服务异常。

示例代码展示了异步发送评论消息的逻辑:

public function swooleComment($uid, $data)
{
    $url = $this->swooleUrl . "/api/comment/message";
    $commentUid = empty($data['comment_uid']) ? 0 : $data['comment_uid'];
    $msg = [
        "uid" => $uid,
        "msg" => json_encode(['comment_uid' => $commentUid])
    ];
    // 首次尝试,超时2秒
    $res = Curl::posturl($url, http_build_query($msg), $this->_headerQArr, 2);
    if ($res === false) {
        usleep(100000); // 重试间隔100ms
        // 二次尝试
        $res = Curl::posturl($url, http_build_query($msg), $this->_headerQArr, 2);
    }
    return $res;
}

第3步:消息缓冲与双队列处理

目的:解决 WebSocket 未连接时的消息丢失问题,并根据用户活跃度分类存储消息。

痛点:Swoole 在 WebSocket 未建立连接时无法实时通信。因此需要将消息先入队,待用户上线后推送。

操作设计:

  • 数据结构:使用 Redis List 结构构建快慢双队列。
  • 快队列:存放当前最新、高频交互的消息。
  • 延迟队列:若用户超过 1 天未登录,消息转入延迟队列;超过 15 天未登录,则消息被释放(不再强推)。
  • 执行频率:鉴于 Redis 集群单节点稳定性考虑及 WebSocket 心跳时间(300s),Crontab 定时任务每 4 分钟执行一次消息投递处理,延迟队列每 6 分钟检查一次。

第4步:MySQL 分表与异步写入

目的:应对 300w+ 用户规模下的消息海量存储压力,保证主线程高性能。

操作:

  1. 分表策略:基于 uid 进行取模分表。示例中采用 128 张表(user_push_msg_0 ~ user_push_msg_127)。
  2. 异步插入:所有数据库写操作不阻塞主进程,通过 TaskManager 异步执行。
protected function _getTableName(int $uid): string
{
    // 根据 uid 对 128 取模确定分表名
    $tableIndex = intval($uid % 128);
    return 'user_push_msg_' . $tableIndex;
}

protected function addAsyncMysql(array $pushMsg, int $uid): ?bool
{
    $tableName = $this->_getTableName($uid);
    if (empty($pushMsg) || empty($tableName) || empty($uid)) return false;
    
    // 异步执行数据库操作
    TaskManager::getInstance()->async(function () use ($pushMsg, $tableName) {
        DbManager::getInstance()->invoke(function (ClientInterface $client)
        use ($pushMsg, $tableName) {
            $model = PushMsgModel::invoke($client, $pushMsg);
            $model->tableName($tableName)->save();
        }, self::MYSQL_CONN_NAME);
    });
}

第5步:用户消息数统计方案

目的:精确统计各分组(全体、作者、签约作者等)用户的未读消息数,特别是群组类消息的复杂累加场景。

难点:传统方式难以准确区分“已读”与“未读”,尤其是多用户群发消息。

两步解决法:

  1. 增量计数器:在 Http 消息接收端,每接收一条针对某用户的消息,计数器加 1。这代表了“产生但未确认”的消息总量。
  2. 最后读取时间戳表:新建一张专用统计表 message_read_time,以 uid 为主键。记录用户最后一次查看消息的时间戳。

统计逻辑:当需要展示未读数时,查询该用户所属分组的总消息量,减去在该用户“最后读取时间”之后产生的新消息量。或使用以下结构更新读取行为:

抓要源文编写(此处为更新操作示例)
-- 表结构设计
CREATE TABLE `message_read_log` (
  `uid` int(10) unsigned NOT NULL DEFAULT '0',
  `unixtime` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户查看消息最新时间';

-- 更新方式
-- REPLACE INTO ... -- 保证如果存在则更新时间,不存在则插入

最终结果:用户未读数 = 基础计数器数值 - (在 unixtime 之后新增且属于该用户的消息量)。这种分离存储与计数的方法兼顾了实时性与准确性。

以上就是基于 Swoole 的 WebSocket 消息服务系统方案设计详解,更多关于 Swoole WebSocket 消息服务的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表