引言
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
版本
Version PHP (*) Release Bug Fixes Until Security Fixes Until
6 (LTS) 7.2 - 8.0 September 3rd, 2019 January 25th, 2022 September 6th, 2022
7 7.2 - 8.0 March 3rd, 2020 October 6th, 2020 March 3rd, 2021
8 7.3 - 8.1 September 8th, 2020 July 26th, 2022 January 24th, 2023
9 8.0 - 8.1 February 8th, 2022 August 8th, 2023 February 6th, 2024
10 8.1 - 8.3 February 14th, 2023 August 6th, 2024 February 4th, 2025
11 8.2 - 8.3 March 12th, 2024 September 3rd, 2025 March 12th, 2026
12 8.2 - 8.3 Q1 2025 Q3 2026 Q1 2027
学习
参考:20个最佳Laravel免费和付费教程资源 https://www.wbolt.com/laravel-tutorial.html
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
Inertia
Inertia 是一种构建经典服务器驱动型网络应用程序的新方法。我们称之为现代单体。 Inertia 允许您创建完全由客户端渲染的单页面应用程序,而不会像现代 SPA 那样复杂。Inertia 通过利用现有的服务器端模式来实现这一目标。 Inertia 没有客户端路由,也不需要 API。只需像以往一样构建控制器和页面视图即可! Inertia 与任何后端框架都能完美配合,但它针对 Laravel 进行了微调。
简单来说就是可以在后端项目代码开发中,直接写现在单页面及时响应前端代码,避免了前后端分离成两个项目的弊端,减少了项目维护成本。
示例项目,Laravel 结合Vue 开发的CRM项目 https://gitee.com/ibaiyang/pingcrm, 其中使用到了 inertia,php^7.3,laravel^8.0,vue^2.6.11。 在线体验地址 https://demo.inertiajs.com
Jetstream
Laravel Jetstream 是一款设计精美的 Laravel 应用程序入门套件,为下一个 Laravel 应用程序提供了完美的起点。 Jetstream 为应用程序提供登录、注册、电子邮件验证、双因素身份验证、会话管理、通过 Laravel Sanctum 的 API 以及可选的团队管理功能。
官网文档 https://jetstream.laravel.com/introduction.html
鉴于上面原因,可以再向上走一步,直接把Jetstream在Laravel中安装好。项目地址 https://gitee.com/ibaiyang/jetstream1-base, jetstream版本^1.0的一个基础项目,其中使用到了 inertia,php^7.3.0,laravel^8.0。
Laravel Mix
官网文档 https://laravel-mix.com/docs/6.0/what-is-mix
参考资料
不使用pcntl/pthreads/swoole的前提下, laravel该如何实现伪多进程? https://mp.weixin.qq.com/s/tzFV5RKxGZI5Typw2K0BJg