上一篇:swoole4.0之打造自己的web开发框架(6),我们符合了PSR7规范以及模板引擎的引入,框架的能力更加全面了
之前在swoole4.0之打造自己的web开发框架(4)文章中,实现一个完整的CRUD中有一个bug,被一个盆友发现了:
确实是当时手快,有所疏忽,不过这确实是一个很典型的问题,有必要在详细说一说了
问题产生的原因就是:用了全局单例,而且mysql连接获取是在构造函数(__construct)里执行的,导致第一次被初始化之后,后续所有的请求都复用了mysql连接,这样有并发请求会出错,且不能达到请求完成资源归还的目的,也失去了连接池的意义
所以给出的修复方案是:
/**
* @return Mysql
* @throws \Exception
*/
public function getDb()
{
$coId = Coroutine::getId();
if (empty($this->dbs[$coId])) {
//不同协程不能复用mysql连接,所以通过协程id进行资源隔离
//达到同一协程只用一个mysql连接,不同协程用不同的mysql连接
if ($this->dbTag) {
$mysqlConfig = Config::get($this->dbTag);
} else {
$mysqlConfig = null;
}
$this->dbs[$coId] = MysqlPool::getInstance($mysqlConfig)->get();
defer(function () {
//利用协程的defer特性,自动回收资源
$this->recycle();
});
}
return $this->dbs[$coId];
}
把获取连接单独拎出来,然后在需要的时候调用获取
$result = $this->getDb()->query($query);
这个bug隐含了swoole代码里各维度生命周期问题,在swoole世界里有以下几个维度
PHP维度的生命周期
这里我们不探讨zendVM的细节,PHP不管哪种运行方式(CLI, FPM, Apache Moudle...)都是遵循SAPI接口, 下图描述了一个php执行的基本流程
抽象出来就是下面5个阶段:
1、模块初始化阶段(Module init) :
即调用每个拓展源码中的的PHP_MINIT_FUNCTION中的方法初始化模块,进行一些模块所需变量的申请,内存分配等。
2、请求初始化阶段(Request init) :
即接受到客户端的请求后调用每个拓展的PHP_RINIT_FUNCTION中的方法,初始化PHP脚本的执行环境。
3、执行PHP脚本
4、请求结束(Request Shutdown) :
这时候调用每个拓展的PHP_RSHUTDOWN_FUNCTION方法清理请求现场,并且ZE开始回收变量和内存。
5、关闭模块(Module shutdown) :
Web服务器退出或者命令行脚本执行完毕退出会调用拓展源码中的PHP_MSHUTDOWN_FUNCTION 方法
以FPM为例,FPM启动时,执行第一步,后续每个请求进来,结束,在重复2~4步,FPM关闭时,执行第5步
在FPM模式下,每个进程同时只能执行一个请求,全局变量,各类资源都会在第4步进行释放,而每个请求也是重复加载文件,资源初始化等,优点就是开发简单,没有上下文关系,所有的资源都圈定在请求以内,缺点就是性能差,需要做很多重复性的工作
swoole server维度的生命周期
swoole在第3个阶段,就会接管了php的生命周期,而swoole也有类似的5个阶段(以http server为例):
1、onStart :
swoole server启动
2、onWorkerStart
swoole的 worker进程程启动,有点类似于Minit
3、onRequest
请求到达到的处理回调, 可以认为是 2~4步的集合
4、onWorkerStop
工作进程结束时的回调,类似于MSHUTDOWN
5、onShutdown :
swoole server关闭时回调,此方法调用之后,才会进入到 PHP的 4 - 5 阶段
swoole onRequest(协程)维度的生命周期
通过对前面两个生命周期的介绍,我们知道在onRequest或swoole协程中,全局变量,资源等是跨请求存在的,因为并不会执行到php的4以后阶段做资源的清理过程,所以在swoole协程中,我们需要有上下文,需要通过 Coid(协程id)进程资源隔离,需要在协程处理完成做一些资源的清理,以防止内存泄露等问题, 当然利用这个,我们也才可以做跨请求的资源共享,可以做各种资源池,可以避免每个请求都做重复的功作,从而大大的提升性能。
单例的陷阱
所以,在swoole代码里,单例要做更细粒度的区分,如果按照之前FPM里的方式来,那很容易就出问题了,这里总结为三个粒度:
1、进程粒度
2、请求粒度
3、协程粒度
那什么场景用什么单例呢?
1、进程级别的单例:无任何共享数据,或者是可以进程间共享(跨请求)的数据(如全局变量),这个级别和FPM一致,大部分情况都是这个级别,特别如框架级别的代码
2、请求级别的单例:请求内可共享的数据,如:waitgroup做并发,就是请求内可以数据共享的, 大部分情况不需要使用,可以通过进程级别单例+数据隔离实现
3、协程级别的单例:请粒度最小,能是协程内的数据共享,这种情况基本不建议使用单例了
4、进程级别可数据隔离单例: 如果我们的单例有非跨请求的数据或资源,那么这个数据和资源就不用能static变量,也不能在构造函数里初始化
下面给出这几个单例的代码实现:
<?php
//file frame/Family/Core/Singleton.php
namespace Family\Core;
use Family\Coroutine\Coroutine;
trait Singleton
{
private static $instance;
private static $coInstances;
/**
* @param mixed ...$args
* @return mixed
* @desc 进程内的全局单例
*/
public static function getInstance(...$args)
{
if (!isset(self::$instance)) {
self::$instance = new static(...$args);
}
return self::$instance;
}
/**
* @param mixed ...$args
* @return mixed
* @desc 协程内的单例
*/
public static function getCoInstance(...$args)
{
$coId = Coroutine::getId();
if (!isset(self::$coInstances[$coId])) {
self::$coInstances[$coId] = new static(...$args);
defer(function () use ($coId) {
unset(self::$coInstances[$coId]);
});
}
return self::$coInstances[$coId];
}
/**
* @param mixed ...$args
* @return mixed
* @desc 请求级别的单例,用此方法:
* @desc 同一请求内开协程,需要调用Family\Coroutine\Coroutine::create方法创建
* @desc 不能直接用祼go创建
*/
public static function getRequestInstance(...$args)
{
$coId = Coroutine::getPId();
if (!isset(self::$coInstances[$coId])) {
self::$coInstances[$coId] = new static(...$args);
defer(function () use ($coId) {
unset(self::$coInstances[$coId]);
});
}
return self::$coInstances[$coId];
}
/**
* @param mixed ...$args
* @return mixed
* @desc 按tag获取单例, 可用于static替换
*/
public static function getInstanceByTag(...$args)
{
$tag = $args[0];
if (!isset(self::$coInstances[$tag])) {
self::$coInstances[$tag] = new static(...$args);
}
return self::$coInstances[$tag];
}
}
期望这篇文章能帮助大家更理解php和swoole
下一篇将介绍:如何实现在代码热更新,以及代码热更新的范围,以及服务管理脚本的实现
查看原文,介绍两本好书:PHP 7底层设计与源码实现+PHP7内核剖析,有兴趣可以看看或购买
github地址: https://github.com/shenzhe/family
----------伟大的分割线-----------
PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!
饭米粒只发原创或授权发表的文章,不转载网上的文章
所发的文章,均可找到原作者进行沟通。
也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投搞。
投稿请联系:
本文由 半桶水 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)
文章评论