EasySwoole消息推送8 - 开发完结

概述

上周结束了年报项目,一切又是新的开始,准备迎接新一场的暴风雨,然后享受暴风雨后的平静的幸福,
好吧,就让我们回顾一下之前项目和项目的问题。

存在的问题:

  • 如果服务器发生故障,Uid-Fd的对应关系得存在?
  • 消息体在推送之前要加以验证,如果存在未读消息,不要复推新的消息?
  • 当数量多的时候,存数据的时候采用什么策略分表?
  • 部署的时候,怎么支持Ws和Wss?
  • 链接(正常/错误)断开的回收?

新的思考:

1.系统初始化

2.需要把消息分成三类:登陆、广播、UID取模

3.优雅编程

4.部署/启动/热启动

0.服务的启动/停止

启动服务,-d是以守护进程的方式启动

php easyswoole server start -mode=websocket -d

停止服务

php easyswoole server stop -mode=websocket
1.系统初始化
需要用程序来初始化数据库创建表、代码实现Crontab
2.用户登录/退出拆分
把用户Uid/Fd的关系单独分拆在一个表中,这样有利于维护关系的稳定性和正确性,优化了缓存和数据库
双重保险。
use EasySwoole\EasySwoole\Task\TaskManager;
use App\Models\PushMsgLoginModel;

//写入缓存
$fd = $this->caller()->getClient()->getFd();
$hRet = $this->redis->hSet(self::PUSH_MSG_USER_LOGIN,$uid, $fd );

//异步写入MySql
TaskManager::getInstance()->async(function () use ( $pushMsg ){
    $model = PushMsgLoginModel::create($pushMsg);
    $model->save();
});
3.消息体存储 - Orm数据库分表
说明:因为每天有15w-30w+的活跃用户在网站上进行实时互动,防止数据过多查询过慢,提前对
数据进行分表设计。

执行程序分表代码,如下:

use EasySwoole\Mysqli\QueryBuilder;
use App\Models\PushMsgModel;

$queryBuild = new QueryBuilder();

// 支持参数绑定 第二个参数非必传
for ($i = 0; $i < 128; $i++) {
    $sql = "表结构略...";
    //创建数据库
    $queryBuild->raw($sql);
    $data = PushMsgModel::create()->query($queryBuild, true);
    var_dump("data:".var_dump($data));
}

分表主要根据业务去处理,业务主要针对用户处理,分表策略采用的是用Uid取模。

protected function _getTableName($uid){
    $tableIndex = intval($uid % 128);
    return 'user_push_msg_'.$tableIndex;
}

在 EasySwoole ORM AbstractModel 有 操作数据库实现方法tableName,路径 ++vendor/easyswoole/orm/src/AbstractModel.php++

分表+Task异步处理,use用来定义匿名函数外的变量值,显得更简捷优雅

protected function addAsyncMysql($pushMsg,$uid){
    $tableName = $this->_getTableName($uid);
    TaskManager::getInstance()->async(function () use ( $pushMsg,$tableName ){
        $model = PushMsgModel::create($pushMsg);
        $model->tableName($tableName)->save();
    });
}
5.(全局/个人)广播消息的处理

广播就是所谓的系统消息,分为全局和个人,思路是一样的,以全局为例,每隔5个小时执行一次Crontab。

Crontab参数:

public static function getRule(): string
{
    return '* 0-23/5 * * *';
}

run 实现如下:

while (true) {
    //在线用户进行推送
    $pushMsgLoginLists = PushMsgLoginModel::create()
        ->field(['push_id','uid','user_fd'])
        ->where('push_id > '.$push_id)
        ->where('user_status',1)
        ->where('user_fd > 0 ')
        ->limit(0,$this->limit )
        ->all()->toArray();
    if (!isset($pushMsgLoginLists) || empty($pushMsgLoginLists)) {
        echo '没有新的消息通知'.PHP_EOL;
        break;
    }
    //在线用户进行推送
    $notifLists = [];
    $pushLists = [];
    foreach ($pushMsgLoginLists as $value){
        $addMsgData = [
            'noce_ack' => $this->getNoceAck(),
            'to_uid' => $value['uid'],
            'is_sent' => 1,
            'is_read' => 0, #消息未读
            'msg_type' => 6,
            'client_type' => 0,
            'msg_extend' => '',
            'create_time' => time(),
            'update_time' => 0
        ];
        $tableName = $this->_getTableName($value['uid']);
        $notifLists[$tableName][] = $addMsgData;
        $pushLists[$value['uid']]['user_fd'] = $value['user_fd'];
        $pushLists[$value['uid']]['noce_ack'] = $addMsgData['noce_ack'];
        $push_id = $value['push_id'];
    }

    foreach ($notifLists as $tableName => $msgData){
        echo 'tableName:'.$tableName.PHP_EOL;
        $model = PushMsgModel::create();
        $model->tableName($tableName)->saveAll($notifLists[$tableName]);
    }

    foreach ($pushLists as $uid => $value){
        $pushMsg = [
            "uid" => 0,
            "msg_type" => 6,
            "code" => 200,
            "msg" => 'SUCCESS',
            "body" => ['to_uid' => $uid ],
            'noce_ack' => $value['noce_ack']
        ];
        $server->push($value['user_fd'],json_encode($pushMsg));
    }
}
6.优雅编程

6.1 toArray数据集优化

如果设置了returnCollection为true,无需进行foreach。可直接:

step.1 设置returnCollection
/** @var \EasySwoole\ORM\Db\Config  $config **/
$config->setReturnCollection(true);
step.2 获取结果集
\EasySwoole\ORM\Tests\models\TestUserModel::create()->all()->toArray();

6.2 批量插入优化

saveAll可以传递二维数组,批量插入数据,但由于ORM的工作职责,他需要将数据映射为对象,所以在内部处理中还是通过遍历处理,而非一条sql插入。

//方法原型
function saveAll($data, $replace = true, $transaction = true)
//used
$model = PushMsgModel::create();
$model->tableName($tableName)->saveAll($notifLists[$tableName]);
7.链接断开的回收

EasySwoole中有onClose方法回收断开链接的回收,文件路径 ++/App/WebSocketEvent.php++

static function onClose(\swoole_server $server, int $fd, int $reactorId)
{
    $info = $server->getClientInfo($fd);
    if ($info && $info['websocket_status'] === WEBSOCKET_STATUS_FRAME)
    {
        //读取所有的fd对应的Uid
        $loginUserLists = PushMsgLoginModel::create()
            ->field(['uid'])
            ->where('user_status',1)
            ->where('user_fd > 0')
            ->where('user_fd',$fd)
            ->all()->toArray();
        //关闭链接,回收uid/fd的关系
        $data = ['user_status' => 0 , 'user_fd' => 0 , 'update_time' => time() ];
        $ret = PushMsgLoginModel::create()->update($data, ['user_fd' => $fd]);
        if($ret && isset($loginUserLists) && !empty($loginUserLists)){
            $uids = array_filter(array_unique(array_column($loginUserLists,'uid')));
            $redisConf = Config::getInstance()->getConf('redis');
            $redis=\EasySwoole\Pool\Manager::getInstance()->get('redis')->getObj();
            $redis->select( $redisConf['REDIS']['select'] );
            //回收对象
            \EasySwoole\Pool\Manager::getInstance()->get('redis')->recycleObj($redis);
            $redis->hMGet('PUSH_MSG_USER_LOGIN',$uids);
        }
    }
}
优化点集合
  • 单一入口权限验证,先查询redis缓存(hash),不存在查询数据库,双重关系存储
  • 拆分用户登陆/链接关系数据表记录,redis (哈希) 缓存
  • 消息通知,分批分块处理
  • 对所有断开链接的websocket,进行回收,清除关系

程序的开发没有最好,只有更好,加油加油再加油,长路漫漫,唯有奋斗!希望对你有所帮助,感谢阅读,学习改变未来~

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 点我我会动 设计师:白松林 返回首页