最新公告
  • 欢迎您光临码农资源网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!加入我们
  • LaravelORM+协程在Webman中的应用

    这几天我在想如何在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中的应用
    • 7会员总数(位)
    • 25846资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 294稳定运行(天)

    提供最优质的资源集合

    立即查看 了解详情