Laravel 使用


Laravel 使用


引言

Laravel 是一个 Web 应用框架, 有着表现力强、语法优雅的特点。Laravel 致力于提供出色的开发体验, 同时提供强大的特性,例如完全的依赖注入,富有表现力的数据库抽象层,队列和计划任务,单元和集成测试等等。

源码地址:https://github.com/laravel/laravel

Laravel 官网:https://laravel.com/

Laravel 学院 - Laravel 6 中文文档:https://laravelacademy.org/books/laravel-docs-6

Laravel 中文网: http://laravel.p2hp.com/

Laravel 中文网 - Laravel 6.*版 中文使用文档: http://laravel.p2hp.com/cndocs/6.x/mix

Laravel 实现伪多进程

不使用pcntl,pthreads,swoole的前提下,Laravel 如何实现伪多进程

一、前言

众所周知,多进程/多线程可以并行/并发的执行多个任务,提高运行效率。

PHP默认是不支持多进程/多线程的,需要安装pcntl/pthreads扩展来支持。 协程如果不用swoole等框架,那么实现比较复杂。

以上方法均不使用,那么该如何提高程序的运行效率呢?

二、思路

对于耗时的任务, 通常会推送到任务队列中,然后队列消费进程从任务队列中获取任务执行。

一个队列是可以开启多个消费进程的,那么执行任务的效率是比单个进程顺序执行效率多很多的。

如果不需要等待所有任务的执行完成来获取结果的话,其实开启多个队列消费进程已经够了。

如果需要等待所有任务完成才返回结果的操作,比如在定时任务中需要读取Mysql的100条记录,去调用第三方的API, 这个三方API很Low,调用一次需要2s,最终需要生成这100条记录的CSV文件。顺序执行至少需要200s才能完成, 如果有4个队列消费进程, 那么只需要50s左右即可完成。

主要问题在于如何解决进程间通信?因为需要知道这些子任务是否执行完,以及需要知道任务的执行结果。 那么是否可以将使用redis来通信呢?用redis进行计数操作,每个任务执行完将计数器加1, 每个任务的执行结果都放在redis的list/hash中,当计数器的总数等于任务总数的时候, 就可以断定任务已经执行完成,然后取出redis存放的结果,生成csv文件。

三、实现

实际项目已在线上稳定跑了几个月了,在这里只贴伪代码展示一下实现思路,有兴趣的可以自己实现试试。

定时任务command,这里主要是程序入口, 设置好计数器, 并且等待队列任务执行完成,获取结果。

/**
*    定时任务
*    Author:ClassmateLin
*    Email:classmatelin.site@gmail.com
*    Site:https://www.classmatelin.top
*/
public function handle()
{
    $recordL1st=[1,2,3,4,5,6,7,8,9,10];
    $taskService = app(abstract:TaskService:class);
    $taskService->init(count($recordList));//初始化计数器,设置任务总数
    
    foreach ($recordL1st as $record)
    {
        //将任务推送到calculate队列中
        CalculateJob:dispatch($record)->onQueue(queue:'calculate');
    }
    
    //这里等待任务完成
    while ($taskService->isFinish()){
        sleep(seconds:1);
    }
    
    /获取任务结果
    $result = $taskService->getResult();
}

具体任务Job代码, 主要执行耗时任务,以及执行完成计数器进行加1,需要注意的是异常也要保证计数器+1, 否则任务总数跟执行总数不相等,那么主进程command会卡死。

/**
* 执行具体耗时任务
* Author:ClassmateLin
* Email:classmatelin.site@gmail.com
* Site:https://www.classmatelin.top
*/
public function handle()
{
    try {
        sleep(seconds::2);//执行任务
        $this->taskService->addResult($this->job->getJobId(),result::123);//保存结果到redis
        
        $this->taskService->do();//计数器+1
    } catch (\Exception Se) {
        /记录错误
        $this->taskService->do();/需要注意失败计数器也+1,要保证执行总数等于任务总数,不然主进程会卡死。
    }
}

将任务相关信息保存到redis的hash数据结构中, 需要保存任务总数的key, 用于统计的已执行任务总数的key, 计数可以用时间复杂度为O(1)的hincrby命令。

{
    protected ShashTableName 'counter:hash';//hash table name
    protected StotalTaskKey 'total_task:count';/total
    protected SexecutedTaskKey 'executed_task:count';/executed
    protected $LockKey='counter:Lock';/锁名称
    
    /** 初始化
        Author:ClassmateLin
        Email:classmatelin.site@gmail.com
        Site:https://www.classmatelin.top
        @param Scount
    */
    public function init(Scount)
    {
        app(abstract:'redis.connection')->hset($this->hashTableName,$this->totaLTaskKey,$count);
    }
    
    /** 计数器+1
        Author:ClassmateLin
        Email:classmatelin.site@gmail.com
        Site:https://www.classmatelin.top
    */
    public function do()
    {
        do {
            $isLock = app(abstract:'redis.connection')->set($this->LockKey,1,'ex',1,'nx');
            if (sisLock){
                app(abstract:'redis.connection')->hincrby($this->hashTableName,$this->executedTaskKey,1);   
            }
        }while(!$isLock);
    }
    
}

结果也可以保存到该hash中, 通过hvals获取结果前先把两个count的key删除即可。

public function isFinish():bool
{
    $executedCount = app(abstract:'redis.connection')->hget($this->hashTableName,$this->executedTaskKey);
    /其实定义常量就行了。
    $totalCount = app(abstract:'redis.connection')->hget($this->hashTableName,$this->totalTaskKey);
    
    return $executedCount = $totalCount;
}

/**
    获取最终结果
    Author:ClassmateLin
    Email:classmatelin.site@gmail.com
    Site:https://www.classmatelin.top
*/
public function getResult()
{
    app(abstract:'redis.connection')->hdel($this->hashTabLeName,$this->totaLTaskKey,$this->executedTaskKey);
    return app(abstract:'redis.connection')->hvals($this->hashTableName);
}

/**
    保存一条结果
    Author:ClassmateLin
    Email:classmatelin.site@gmail.com
    Site:https://www.classmatelin.top
    dparam $jobId
    dparam Sresult
*/
public function addResult($jobId,Sresult)
{
    app(abstract:'redis.connection')->hset($this->hashTableName,$jobId,$result);
}

该代码示例只适合学习使用,如果需要生产使用需要自己处理细节问题。

开启多个任务队列, 可手动开启几个终端手动执行:php artisan queue:work redis --queue=calculate启动多个队列消费进程, 也可以通过supervisor来启动, 通过配置numprocs=8参数来限制进程数量。

Redis事务

Laravel的官方文档中对Redis事务的使用并没有做介绍,https://laravelacademy.org/post/19973 ,所以这里记录一下具体示例。 具体Redis事务的文档,可以参阅 http://doc.redisfans.com/topic/transaction.html

这里举一个 Laravel 框架中 Redis事务 使用的例子:

use Illuminate\Support\Facades\Redis;

......

$uuid = 100001;

try {
    Redis::multi();
    
    Redis::set($uuid, 'uuid:' . $uuid);  
    Redis::hMSet('user:' . $uuid, ['id' => $uuid, 'uuid' => $uuid]]);
    Redis::lPush('uuid_list', $uuid);
    Redis::sAdd('user_id_key', $uuid);

    $results = Redis::exec();
} catch (\Exception $e) {
    Redis::discard();

    throw new \Exception('Redis缓存数据保存失败,' . $e->getMessage());
}

结合 数据库事务 使用 Redis事务 再举个例子:

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;

......

$uuid = 100001;

DB::beginTransaction();
try {
    // mysql
    DB::insert('insert into users (uuid, content) values (?, ?)', [$uuid, 'uuid:' . $uuid]);

    // Redis
    try {
        Redis::multi();
        
        Redis::set($uuid, 'uuid:' . $uuid);  
        Redis::hMSet('user:' . $uuid, ['id' => $uuid, 'uuid' => $uuid]]);
        Redis::lPush('uuid_list', $uuid);
        Redis::sAdd('user_id_key', $uuid);
    
        $results = Redis::exec();
    } catch (\Exception $e) {
        Redis::discard();
    
        throw new \Exception('Redis缓存数据保存失败,' . $e->getMessage());
    }

    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw new \Exception('数据保存失败,' . $e->getMessage());
}

Laravel的数据库事务,可以参阅 https://laravelacademy.org/post/19968

查看SQL执行语句

开发中碰到这样一种情况,你想查看Model查询时具体的SQL执行语句怎么办?

你可以使用 DB::enableQueryLog() 方法,如下:

// 启用查询日志  
DB::enableQueryLog();

// 具体执行逻辑
$model = Mails::query()->select('mails.*');
$model->leftJoin('user as u1', 'mails.to', '=', 'u1.uuid')->where('u1.type', 1);
$records = $model->orderByDesc('mails.id')->paginate(10)->toArray();
$total = $records['total'];
$data = $records['data'];

// 获取查询日志  
$queries = DB::getQueryLog();  

// 打印最后一个查询的 SQL 语句  
if (!empty($queries)) {  
    $lastQuery = end($queries);  
    $sql = $lastQuery['query'];  
    $bindings = $lastQuery['bindings'];  
    $sqlWithBindings = str_replace(array('%', '?'), array('%%', "'" . implode("','", $bindings) . "'"), $sql);  
    echo $sqlWithBindings;  
}

输出:select `mails`.* from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = '1' order by `mails`.`id` desc limit 10 offset 0

看一下上面 $queries 的具体内容:

array(2) {
  [0]=>
  array(3) {
    ["query"]=>
    string(118) "select count(*) as aggregate from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = ?"
    ["bindings"]=>
    array(1) {
      [0]=>
      int(1)
    }
    ["time"]=>
    float(162.54)
  }
  [1]=>
  array(3) {
    ["query"]=>
    string(151) "select `mails`.* from `mails` left join `user` as `u1` on `mails`.`to` = `u1`.`uuid` where `u1`.`type` = ? order by `mails`.`id` desc limit 10 offset 0"
    ["bindings"]=>
    array(1) {
      [0]=>
      int(1)
    }
    ["time"]=>
    float(29.69)
  }
}

命令行脚本

在 Laravel 6 中,可以通过编写 Artisan 命令行来创建自己的脚本。 Artisan 是 Laravel 的命令行界面,它提供了大量的工具来执行各种常见的开发任务,也可以编写自己的 Artisan 命令来处理自定义任务。

以下是如何编写 Laravel 6 中的自定义 Artisan 命令的基本步骤:

1、创建命令类

使用 Artisan 命令行工具来生成一个新的命令类:

php artisan make:command YourCommandName

这将在 app/Console/Commands 目录下创建一个新的命令类文件。

2、编辑命令类

打开生成的命令类文件,可以看到 signature 和 description 属性,以及 handle 方法。

signature 属性定义了命令的名称和参数。
description 属性提供了命令的简短描述。
handle 方法是命令的主体,其中包含了你希望执行的代码。

如:

<?php  
 
namespace App\Console\Commands;  
  
use Illuminate\Console\Command;  
  
class YourCommandName extends Command  
{  
    /**  
     * The name and signature of the console command.  
     *  
     * @var string  
     */  
    protected $signature = 'your-command-name {--option=}';  
  
    /**  
     * The console command description.  
     *  
     * @var string  
     */  
    protected $description = 'Command description';  
  
    /**  
     * Create a new command instance.  
     *  
     * @return void  
     */  
    public function __construct()  
    {  
        parent::__construct();  
    }  
  
    /**  
     * Execute the console command.  
     *  
     * @return int  
     */  
    public function handle()  
    {  
        $option = $this->option('option');  
  
        // 在这里编写你的代码逻辑  
        $this->info('Your command has been executed with option: ' . $option);  
    }  
}

3、注册命令

在 app/Console/Kernel.php 文件的 $commands 数组中注册命令,这样 Laravel 就知道要加载它了。

protected $commands = [  
    // ...  
    \App\Console\Commands\YourCommandName::class,  
];

4、运行命令

通过 Artisan 命令行来运行自定义命令:

> php artisan your-command-name --option=value






参考资料

不使用pcntl/pthreads/swoole的前提下, laravel该如何实现伪多进程? https://mp.weixin.qq.com/s/tzFV5RKxGZI5Typw2K0BJg


返回