这几天我在想如何在WEBman框架中使用LaravelORM并支持协程。将两者结合起来,理论上可以兼顾高并发与开发效率。
实验目标
在Webman中集成LaravelORM协程版,并验证其性能和兼容性。
实验准备
环境配置
审计LaravelORM
类
IlluminateDatabaseConnection
publicfunctionselect($query, $bindings =[], $useReadPdo =true){return $this->run($query, $bindings,function($query, $bindings)use($useReadPdo){if($this->pretending()){return[];}// For select statements, we'll simply execute the query and return an array// of the database result set. Each element in the array will be a single// row from the database table, and will either be an array or objects.
$statement = $this->prepared(
$this->getPdoForSelect($useReadPdo)->prepare($query));
$this->bindValues($statement, $this->prepareBindings($bindings));
$statement->execute();return $statement->fetchAll();});}
以select方法为例可以看到上述类中,Laravel将所有对PDO的操作都封装在了Connection中
并提供了ConnectionInterface
的抽象接口,这意味着如果实现了这个接口,就可以无缝的替换掉PDO逻辑
施工过程
我选用了AMPHP的Mysql客户端库amphp/mysql
来实现这个接口
<?php declare(strict_types=1);useAmpMysqlMysqlConfig;useAmpMysqlMysqlConnectionPool;useAmpMysqlMysqlTransaction;useClosure;useException;useFiber;useGenerator;useIlluminateDatabaseMySqlConnection;useThrowable;usefunction boolval;usefunction in_array;usefunction spl_object_hash;usefunction trim;classPConnectionextendsMySqlConnection{privateconst ALLOW_OPTIONS =['host','port','user','passWord','db','charset','collate','compression','local-infile','username','database'];/*** @var MysqlConnectionPool */privateMysqlConnectionPool $pool;/**
* @param $pdo
* @param string $database
* @param string $tablePrefix
* @param array $config
*/publicfunction __construct($pdo,string $database ='',string $tablePrefix ='', array $config =[]){
parent::__construct($pdo, $database, $tablePrefix, $config);
$dsn ='';foreach($config as $key => $value){if(in_array($key,static::ALLOW_OPTIONS,true)){if(!$value){continue;}
$key = match ($key){'username'=>'user','database'=>'db',default=> $key
};
$dsn .="{$key}={$value} ";}}
$config =MysqlConfig::fromString(trim($dsn));
$this->pool =newMysqlConnectionPool($config);// if (isset($this->pdo)) {// unset($this->pdo);// }}/**
* @return void
*/publicfunction beginTransaction():void{
$transaction = $this->pool->beginTransaction();;if($fiber =Fiber::getCurrent()){
$this->fiber2transaction[spl_object_hash($fiber)]= $transaction;}else{
$this->fiber2transaction['main']= $transaction;}}/**
* @return void
* @throws Exception
*/publicfunction commit():void{if($fiber =Fiber::getCurrent()){
$key = spl_object_hash($fiber);}else{
$key ='main';}if(!$transaction = $this->fiber2transaction[$key]??null){thrownewException('Transaction not found');}
$transaction->commit();
unset($this->fiber2transaction[$key]);}/**
* @param $toLevel
* @return void
* @throws Exception
*/publicfunction rollBack($toLevel =null):void{if($fiber =Fiber::getCurrent()){
$key = spl_object_hash($fiber);}else{
$key ='main';}if(!$transaction = $this->fiber2transaction[$key]??null){thrownewException('Transaction not found');}
$transaction->rollback();
unset($this->fiber2transaction[$key]);}/**
* @var MysqlTransaction[]
*/private array $fiber2transaction =[];/**
* @param Closure $callback
* @param int $attempts
* @return mixed
* @throws Throwable
*/publicfunction transaction(Closure $callback, $attempts =1): mixed
{
$this->beginTransaction();try{
$result = $callback($this->getTransaction());
$this->commit();return $result;}catch(Throwable $e){
$this->rollBack();throw $e;}}/**
* @return MysqlTransaction|null
*/privatefunction getTransaction():MysqlTransaction|null{if($fiber =Fiber::getCurrent()){
$key = spl_object_hash($fiber);}else{
$key ='main';}if(!$transaction = $this->fiber2transaction[$key]??null){returnnull;}return $transaction;}/**
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/publicfunctionselect($query, $bindings =[], $useReadPdo =true): mixed
{return $this->run($query, $bindings,function($query, $bindings)use($useReadPdo){if($this->pretending()){return[];}
$statement = $this->pool->prepare($query);return $statement->execute($this->prepareBindings($bindings));});}/**
* @param string $query
* @param array $bindings
* @return bool
*/publicfunction statement($query, $bindings =[]):bool{return $this->run($query, $bindings,function($query, $bindings){if($this->pretending()){return[];}
$statement = $this->getTransaction()?->prepare($query)?? $this->pool->prepare($query);return boolval($statement->execute($this->prepareBindings($bindings)));});}/**
* 针对数据库运行 select 语句并返回所有结果集。
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return array
*/publicfunction selectResultSets($query, $bindings =[], $useReadPdo =true): array
{return $this->run($query, $bindings,function($query, $bindings)use($useReadPdo){if($this->pretending()){return[];}
$statement = $this->pool->prepare($query);
$result = $statement->execute($this->prepareBindings($bindings));
$sets =[];while($result = $result->getNextResult()){
$sets[]= $result;}return $sets;});}/**
* 针对数据库运行 select 语句并返回一个生成器。
*
* @param string $query
* @param array $bindings
* @param bool $useReadPdo
* @return Generator
*/publicfunction cursor($query, $bindings =[], $useReadPdo =true):Generator{while($record = $this->select($query, $bindings, $useReadPdo)){yield $record;}}/**
* 运行 SQL 语句并获取受影响的行数。
*
* @param string $query
* @param array $bindings
* @return int
*/publicfunction affectingStatement($query, $bindings =[]):int{return $this->run($query, $bindings,function($query, $bindings){if($this->pretending()){return0;}// 对于更新或删除语句,我们想要获取受影响的行数// 通过该语句并将其返回给开发人员。我们首先需要// 执行该语句,然后我们将使用 PDO 来获取受影响的内容。
$statement = $this->pool->prepare($query);
$result = $statement->execute($this->prepareBindings($bindings));
$this->recordsHaveBeenModified(($count = $result->getRowCount())>0);return $count;});}/**
* @return void
*/publicfunction reconnect(){//TODO: 无事可做}/**
* @return void
*/publicfunction reconnectIfMissinGConnection(){//TODO: 无事可做}}
取代工厂
实现了Connection之后我们还有Hook住DatabaseManager的工厂方法`
returnnewclass($app)extendsConnectionFactory{/**
* Create a new connection instance.
*
* @param string $driver
* @param PDO|Closure $connection
* @param string $database
* @param string $prefix
* @param array $config
* @return SQLiteConnection|MariaDbConnection|MySqlConnection|PostgresConnection|SqlServerConnection|Connection
*
*/protectedfunction createConnection($driver, $connection, $database, $prefix ='', array $config =[]):SQLiteConnection|MariaDbConnection|MySqlConnection|PostgresConnection|SqlServerConnection|Connection{return match ($driver){'mysql'=>newPConnection($connection, $database, $prefix, $config),'mariadb'=>newMariaDbConnection($connection, $database, $prefix, $config),'pgsql'=>newPostgresConnection($connection, $database, $prefix, $config),'sqlite'=>newSQLiteConnection($connection, $database, $prefix, $config),'sqlsrv'=>newSqlServerConnection($connection, $database, $prefix, $config),default=>thrownewInvalidArgumentException("Unsupported driver [{$driver}]."),};}}
为了验证上述无缝耦合的最终效果,我准备将它安装到Webman
接入过程
我封装了一个
Database
类,用于Hook住Laravel的DatabaseManager
useIlluminateContainerContainer;useIlluminateDatabaseCapsuleManager;useIlluminateDatabaseDatabaseManager;useIlluminateEventsDispatcher;useIlluminatePaginationCursor;useIlluminatePaginationCursorPaginator;useIlluminatePaginationPaginator;usePscDriveLaravelCoroutineDatabaseFactory;usefunction class_exists;usefunction config;usefunction get_class;usefunction method_exists;usefunction request;classDatabaseextendsManager{/**
* @return void
*/publicstaticfunction install():void{/**
* 判断是否安装Webman
*/if(!class_exists(supportContainer::class)){return;}/**
* 判断是否曾被Hook
*/if(isset(parent::$instance)&& get_class(parent::$instance)===Database::class){return;}/**
* Hook webman LaravelDB
*/
$config = config('database',[]);
$connections = $config['connections']??[];if(!$connections){return;}
$app =Container::getInstance();/**
* Hook数据库连接工厂
*/
$capsule =newDatabase($app);
$default = $config['default']??false;if($default){
$defaultConfig = $connections[$config['default']]??false;if($defaultConfig){
$capsule->addConnection($defaultConfig);}}foreach($connections as $name => $config){
$capsule->addConnection($config, $name);}if(class_exists(Dispatcher::class)&&!$capsule->getEventDispatcher()){
$capsule->setEventDispatcher(supportContainer::make(Dispatcher::class,[Container::getInstance()]));}// Set as global
$capsule->setAsGlobal();
$capsule->bootEloquent();// Paginatorif(class_exists(Paginator::class)){if(method_exists(Paginator::class,'queryStringResolver')){Paginator::queryStringResolver(function(){
$request = request();return $request?->queryString();});}Paginator::currentPathResolver(function(){
$request = request();return $request ? $request->path():'/';});Paginator::currentPageResolver(function($pageName ='page'){
$request = request();if(!$request){return1;}
$page =(int)($request->input($pageName,1));return $page >0? $page :1;});if(class_exists(CursorPaginator::class)){CursorPaginator::currentCursorResolver(function($cursorName ='cursor'){returnCursor::fromEncoded(request()->input($cursorName));});}}
parent::$instance = $capsule;}/**
* Hook Factory
* @return void
*/protectedfunction setupManager():void{
$factory =newFactory($this->container);
$this->manager =newDatabaseManager($this->container, $factory);}}
实践运行结果
为了更直观的展现协程的效果,我将webman-worker数量改为了1,并且在每次请求中都会进行数据库查询
初始化控制器
/**
* @param Request $request
* @return string
*/publicfunction index(Request $request):string{// 手动Hook调DatabaseManagerDatabase::install();// 记录执行时间
$startTime = microtime(true);// 模拟一个耗时1s的查询
$result =Db::statement('SELECT SLEEP(1);');// 记录结束时间
$endTime = microtime(true);// 输出结果return"{$startTime} - {$endTime}";}
启动单元测试
<?php declare(strict_types=1);namespaceTests;useGuzzleHttpClient;usePHPUnitFrameworkTestCase;usePscPluginsGuzzlePHandler;usefunction Pasync;usefunction Ptick;classCoroutineTestextendsTestCase{publicfunction test_main():void{
$client =newClient(['handler'=>newPHandler(['pool'=>0])]);for($i =0; $i <100; $i++){
async(function()use($client, $i){
$response = $client->get('http://127.0.0.1:8787/');
$responseContent = $response->getBody()->getContents();
echo "Request $i: $responseContentn";});}
tick();
$this->assertEquals(1,1);}}
最终输出结果
可以看到每个请求都确实耗时一秒,但100个请求都在一秒内完成了
Request0:1723015194.3121-1723015195.4183Request1:1723015194.3389-1723015195.4193Request2:1723015194.339-1723015195.4196Request3:1723015194.3391-1723015195.4187Request4:1723015194.3391-1723015195.4198Request5:1723015194.3392-1723015195.42Request6:1723015194.3393-1723015195.4202Request7:1723015194.3394-1723015195.4204Request8:1723015194.3394-1723015195.4588Request9:1723015194.3395-1723015195.4595Request10:1723015194.3395-1723015195.4626Request11:1723015194.3396-1723015195.4633Request12:1723015194.3397-1723015195.4653Request13:1723015194.3398-1723015195.4658Request14:1723015194.3398-1723015195.4688Request15:1723015194.3399-1723015195.4726Request16:1723015194.34-1723015195.4735Request17:1723015194.34-1723015195.4774Request18:1723015194.3401-1723015195.48Request19:1723015194.3402-1723015195.4805Request20:1723015194.3402-1723015195.4816Request21:1723015194.3403-1723015195.4818Request22:1723015194.3404-1723015195.4862Request23:1723015194.3404-1723015195.4911Request24:1723015194.3405-1723015195.4915Request25:1723015194.3406-1723015195.4917Request26:1723015194.3406-1723015195.4919Request27:1723015194.3408-1723015195.4921Request28:1723015194.3408-1723015195.4923Request29:1723015194.3409-1723015195.4925Request30:1723015194.3409-1723015195.4933Request31:1723015194.341-1723015195.4935Request32:1723015194.341-1723015195.4936Request33:1723015194.3411-1723015195.4938Request34:1723015194.3412-1723015195.494Request35:1723015194.3412-1723015195.4941Request36:1723015194.3413-1723015195.4943Request37:1723015194.3414-1723015195.4944Request38:1723015194.3414-1723015195.4946Request39:1723015194.3416-1723015195.4947Request40:1723015194.3417-1723015195.4949Request41:1723015194.3418-1723015195.495Request42:1723015194.342-1723015195.5174Request43:1723015194.3421-1723015195.518Request44:1723015194.3423-1723015195.5184Request45:1723015194.3425-1723015195.5191Request46:1723015194.3426-1723015195.5194Request48:1723015194.3429-1723015195.5215Request50:1723015194.3433-1723015195.5219Request51:1723015194.3435-1723015195.5221Request47:1723015194.3428-1723015195.5225Request49:1723015194.3431-1723015195.523Request52:1723015194.3436-1723015195.5265Request53:1723015194.3437-1723015195.5268Request54:1723015194.3439-1723015195.527Request55:1723015194.344-1723015195.5275Request56:1723015194.3443-1723015195.5282Request57:1723015194.3444-1723015195.5314Request58:1723015194.3445-1723015195.5316Request59:1723015194.3445-1723015195.5318Request60:1723015194.3446-1723015195.5323Request61:1723015194.3448-1723015195.5324Request62:1723015194.3449-1723015195.5326Request63:1723015194.345-1723015195.5328Request64:1723015194.3451-1723015195.533Request65:1723015194.3453-1723015195.5331Request66:1723015194.3455-1723015195.5437Request67:1723015194.3456-1723015195.5441Request69:1723015194.3458-1723015195.5443Request70:1723015194.3459-1723015195.5445Request71:1723015194.346-1723015195.5448Request72:1723015194.3464-1723015195.5451Request68:1723015194.3457-1723015195.5456Request73:1723015194.3471-1723015195.5508Request74:1723015194.3475-1723015195.551Request75:1723015194.3478-1723015195.5512Request76:1723015194.3482-1723015195.5516Request77:1723015194.3486-1723015195.5518Request78:1723015194.3489-1723015195.5542Request79:1723015194.3491-1723015195.5545Request80:1723015194.3492-1723015195.5549Request81:1723015194.3493-1723015195.5605Request82:1723015194.3493-1723015195.561Request83:1723015194.3494-1723015195.5633Request84:1723015194.3494-1723015195.5638Request85:1723015194.3495-1723015195.5641Request86:1723015194.3496-1723015195.5661Request87:1723015194.3496-1723015195.5678Request88:1723015194.3497-1723015195.5681Request89:1723015194.3499-1723015195.5684Request90:1723015194.35-1723015195.5685Request91:1723015194.3501-1723015195.5756Request92:1723015194.3502-1723015195.5758Request93:1723015194.3504-1723015195.576Request94:1723015194.3505-1723015195.5768Request95:1723015194.3506-1723015195.577Request96:1723015194.3508-1723015195.5772Request97:1723015194.3509-1723015195.5774Request98:1723015194.3509-1723015195.5777Request99:1723015194.351-1723015195.5781
结论
通过上述实践,我们可以看到LaravelORM与Webman中使用协程是非常具有可行性的,
并且在高并发&慢查询场景下,协程的优势也在此得到了充分的体现
想要了解更多内容,请持续关注码农资源网,一起探索发现编程世界的无限可能!
本站部分资源来源于网络,仅限用于学习和研究目的,请勿用于其他用途。
如有侵权请发送邮件至1943759704@qq.com删除
码农资源网 » LaravelORM+协程在Webman中的应用
本站部分资源来源于网络,仅限用于学习和研究目的,请勿用于其他用途。
如有侵权请发送邮件至1943759704@qq.com删除
码农资源网 » LaravelORM+协程在Webman中的应用