LUMEN框架下使用Redis队列执行数据库任务

在LUMEN的基础上开发了一个WEB API脚手架。出于安全审计和性能统计的目的,使用了一个前置中间件,当请求进来时/响应发送后,对请求/响应数据进行安全处理然后放入Redis队列,再由队列处理程序将其入库。

但自队列守护进程启动后,由于其它客户端并未持续调用WEB API,8小时后,MySql自动 aborted 了队列守护进程持有的PDO连接,导致此后的请求发生 server has gone away 错误。

经过一番研究,修正代码后,解决了这一不是BUG的BUG。

这是全网唯一正确的解法。

环境:Windows,PHP 7, Redis,LUMEN 8

代码如下:

请求日志表模型类

 1 use Illuminate\Database\Eloquent\Relations\Pivot;
 2 
 4 class RequestLogModel extends Pivot
 5 {
 6 
 7     protected $connection = 'dbName';
 8 
 9     protected $table = 'logs_table';
10 
11     protected $primaryKey = 'primary_key';
12 
13     public $incrementing = true;
14 
15     public $timestamps = false;
16 
17     protected $hidden = [...];
18 
19     protected $fillable = [...];
20 
21     protected $guarded = [...];
22 
23     public $attributes = [...];
24 
25     protected $casts = [...];
26 
27     protected $observables = [...];
28 
29     public function __construct(array $attributes = []) {
30         parent::__construct($attributes);
31     }
32 
33     // 该方法用于定时查询数据库,以维持连接。
34     public static function ping() {
35         return self::query()->limit(1)->select('col')->get('col')->count();
36     }
37 
38     // 日志入库
39     public static function saveLog($content) {
40         return self::query()->insert($content);
41     }
42 
43 }

 

响应日志表模型类(与请求日志表模型类结构完全相同,此处略)

 

请求日志作业类

 1 use App\Jobs\Job;
 2 use Illuminate\Support\Facades\Log;
 3 
 4 class RequestLogJob extends Job
 5 {
 6 
 7     /**
 8      * @var array
 9      */
10     protected $logs;
11 
12     public function __construct($logs) {
13         $this->logs = $logs;
14         $this->delay(3);
15     }
16 
17     public function handle() {
18         RequestLogModel::saveLog($this->logs);
19     }
20 
21     public function failed() {}
22 
23 }

 

响应日志作业类(与请求日志作业类结构完全相同,此处略)

 

数据库连接保活作业类

 1 use App\Jobs\Job;
4 class KeepConnectionJob extends Job 5 { 6 7 public function __construct() { 8 $this->delay(1); 9 } 10 11 public function handle() { 12 // 调用请求日志表模型类的 ping 方法 13 RequestLogModel::ping(); 14 // Log::debug('心跳发送成功.'); 15 } 16 17 public function failed() {} 18 19 }

 

自定义控制台命令类

 1 use Illuminate\Console\Command;
 2 
 3 class KeepConnectionCommand extends Command
 4 {
 5 
 6     protected $signature = 'ping_db';
 7 
 8     protected $description = '数据库连接保活.';
 9 
10     public function __construct() {
11         parent::__construct();
12     }
13 
14     // 保活处理
15     public function handle() {
16         /*
17          * 将 KeepConnectionJob 作业推入日志队列,
18          * 实现与日志队列共享同一个连接并定时使用该连接向数据库发送心跳。
19          */
20         dispatch(new KeepConnectionJob());
21     }
22 
23 }

 

注册自定义的控制台命令(在 app\Console\Kernel.php 中加入命令)

 1 class Kernel extends ConsoleKernel
 2 {
 3 
 4     protected $commands = [
 5         KeepConnectionCommand::class, // 自定义的控制台命令类
 6     ];
 7     
// 命令调度 8 protected function schedule(Schedule $schedule) { 9 // 每分钟执行一次 ping_db 命令。 10 $schedule->command('ping_db')->everyMinute(); 11 } 12 }

 

日志处理中间件类

 1 use Illuminate\Http\Request;
 2 use Symfony\Component\HttpFoundation\Response;
 3 
 4 class LogMiddleware
 5 {
 6 
 7     /**
 8      * 记录请求
 9      * @param Request $request
10      * @param Closure $next
11      * @return mixed
12      */
13     public function handle(Request $request, \Closure $next) {
14         // Goalkeeper::secureRequest 是一个自定义的对数据进行安全处理的方法
15         if($request = Goalkeeper::secureRequest($request)) {
16             // 向日志队列推入一个新作业
17             dispatch(new RequestLogJob($request));
18             return $next($request, );
19         }
20         return ...
21     }
22 
23     /**
24      * 记录响应
25      * @param Request $request
26      * @param Response $response
27      */
28     public function terminate($request, $response) {
29         // 向日志队列推入一个新作业
30         dispatch(
31             new ResponseLogJob($response)
32         );
33     }
34 
35 }

 

在 bootstrap\app.php 中将日志中间件注册为全局

1 <?php
2 ...
3 
4 // 注册全局中间件(对所有请求)
5 $app->middleware(
6     [LogMiddleware::class],
7 );
8 
9 return $app;

 

最关键的地方来了

在项目根目录下创建一个 ping.bat 文件,文件内容如下:

e:
cd 你的项目目录
你的PHP安装路径\php.exe artisan schedule:run

 

然后,在系统计划任务中创建一个任务,间隔 x 分钟执行一次 ping.bat,实现数据库连接保活。

 

最后,打开 cmd.exe,执行下列命令启动你的日志队列:

e:
cd 你的项目目录

你的PHP安装路径\php.exe artisan queue:work --daemon

 

原理解析:

        一、 "php.exe artisan queue:work --daemon" 命令启动了一个独立的PHP进程,此处把它叫做 "PHP客户端"。该客户端运行中会建立与MySql的连接。当该客户端长时间不向数据库发送操作请求时,数据库会切断与该客户端的连接(这意味着无论该客户端持有多少连接,这些连接都将变得无效)。要维护这个客户端与数据库的连接,保活逻辑必须在该客户端中执行。即:要对哪个客户端的连接实施保活,则保活逻辑必须在该客户端内执行

       二、LogMiddleware 中间件的 handle 方法和 terminate 方法负责将请求数据和响应数据注入 RequestLogJob 和 ResponseLogJob 的新实例,然后调用 LUMEN 框架的助手方法 dispatch 将作业推入日志队列(队列配置请自行查阅LUMEN文档)。

KeepConnectionCommand 类负责将 KeepConnectionJob 作业推入日志队列。上述客户端则负责监听日志队列并处理队列数据(调用 RequestLogJob 和 ResponseLogJob 中的方法保存日志,调用 KeepConnectionJob 中的方法 ping 数据库)。

        三、保活逻辑的工作流:系统计划任务 >> ping.bat >> LUMEN框架的控制台命令调度器 >> app\Console\Kernel->schedule() >> KeepConnectionCommand >> ping_db >> KeepConnectionJob >> RequestLogModel::ping() >> sql query statement...

 

posted @ 2022-07-28 09:48  初壹  阅读(342)  评论(0)    收藏  举报