概述
上周结束了年报项目,一切又是新的开始,准备迎接新一场的暴风雨,然后享受暴风雨后的平静的幸福,
好吧,就让我们回顾一下之前项目和项目的问题。
存在的问题:
- 如果服务器发生故障,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,进行回收,清除关系
程序的开发没有最好,只有更好,加油加油再加油,长路漫漫,唯有奋斗!希望对你有所帮助,感谢阅读,学习改变未来~