Laravel crontab 配置问题

参考

Lnmp环境运行时一般会指定用户www运行。因此配置定时任务的时候,也需要使用www用户来运行定时任务,否则会造成laravel生成的日志是其它用户,导致laravel运行报错,没有日志的可写权限。

配置方法

1.获取当前系统PHP的环境变量

执行 env > /tmp/env.output 然后 cat /tmp/env.output

找到PATH

PATH=/usr/local/mysql/bin:/usr/local/php/bin:/usr/local/nginx/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

2.设置定时任务 -u 参数指定用户 命令参考 给www用户的crontab 添加环境变量PATH

crontab -u www -e

将PATH添加到crontab的第一行,

换行后将laravel的定时任务代码加上,截图中第二行, 制定了环境变量后,可以只写php 不用写php的完整路径 /usr/local/php/bin/php

3,不能遗漏的关键一步

在home目录下创建对应用户的文件目录,www 并修改用户权限为www 最后重启定时任务

cd home && mkdir www && chown -R www.www www
service crond restart

如果没有该用户的目录,crontab日志会有报错

(CRON) ERROR chdir failed (/home/www): No such file or directory

知识总结

Crontab详细介绍可以头部的参考链接,和《鸟哥linux私房菜-基础学习篇》第16章

任务调度分为两类: 系统任务调度 和 用户任务调度 

系统任务配置/etc/crontab

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/
# For details see man 4 crontabs

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name  command to be executed

前四行是用来配置crond任务运行的环境变量,

  • 第一行SHELL变量指定了系统要使用哪个shell,这里是bash,
  • 第二行PATH变量指定了系统执行命令的路径,
  • 第三行MAILTO变量指定了crond的任务执行信息将通过电子邮件发送给root用户,如果MAILTO变量的值为空,则表示不发送任务执行信息给用户,
  • 第四行的HOME变量指定了在执行命令或者脚本时使用的主目录。
  • 第五行 前五个星号代表时间, user-name代表执行的用户,command代表执行的命令

crontab – u xxx -e 是用来设置用户系统任务调度的

所有用户定义的crontab文件都被保存在/var/spool/cron目录中。其文件名与用户名一致,使用者权限文件如下

/etc/cron.deny     该文件中所列用户不允许使用crontab命令
/etc/cron.allow    该文件中所列用户允许使用crontab命令
/var/spool/cron/   所有用户crontab文件存放的目录,以用户名命名

注意单独用户的crontab配置需要设置 PATH 并创建对应用户目录

service crond start    # 启动服务
service crond stop     # 关闭服务
service crond restart  # 重启服务
service crond reload   # 重新载入配置
service crond status   # 查看状态

PHP开源项目

参考

框架类

PHP框架

PECL扩展框架

类库工具

图像处理

文件处理

  • CSV – CSV数据操作
  • Flysystem  文件系统抽象层,readme中包含很多第三方云服务的扩展包

Office文档处理

爬虫

Laravel 踩坑记录之路由与中间件,论规范的重要性

使用一种技术,就要遵循它的规范,尤其是在没有完全了解实现原理的时候,不能随意的DIY否则就会进入坑中

不明原因(可能的原因同事对框架做了修改)导致在控制器构造方方中使用$this->middleware()方法,terminate中间件没有生效,最后在路由中使用->middleware() 方法中间件生效

Laravel 迁移文件 简单总结

参考

简介

数据库迁移就像是数据库的版本控制,可以让你的团队轻松修改并共享应用程序的数据库结构。迁移通常与 Laravel 的数据库结构生成器配合使用,让你轻松地构建数据库结构。如果你曾经试过让同事手动在数据库结构中添加字段,那么数据库迁移可以让你不再需要做这样的事情。

执行 php artisan migtate 后 数据库中会生成一个迁移文件表 migrations ,每一条记录对应一个执行过的迁移文件,怎么看每次迁移了哪些文件?在 migrations 表中有一个 batch 字段,字段值相同的为同一次迁移

创建 created方法

Schema::create('users', function (Blueprint $table) {
    //...		
});

修改 table方法

数据库因为业务需要变更时,每个表的变更创建一个单独的迁移文件方便生产执行.

需要引入composer require doctrine/dbal 扩展包

Schema::table('migration_demo', function (Blueprint $table) {

});

1 对一个字段做多种修改 例如 重命名和修改类型同时进行.原字段为type 类型int

//无效方式1
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change();
    $table->renameColumn('type', 'demo_type');
    //经测试这两号代码颠倒顺序最后生成的语句是一样的
});

执行语句, 字段重命名时又改回了默认的int类型

ALTER TABLE migration_demo CHANGE type demo_type INT DEFAULT 0 NOT NULL
ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL
//无效方式2
Schema::table('migration_demo', function (Blueprint $table) {
    $table->bigInteger('type')->default('0')->change()->renameColumn('type', 'demo_type');
});

执行语句 ,rename并没有生效

ALTER TABLE migration_demo CHANGE type type BIGINT DEFAULT 0 NOT NULL

同一字段执行多种变更,正确的方式

Schema::table('demo', function (Blueprint $table) {
   $table->renameColumn('name', 'demo_name');
});
Schema::table('demo', function (Blueprint $table) {
   $table->string('demo_name', 255)->default('')->change();
});

执行语句结果

ALTER TABLE demo CHANGE demo_name demo_name VARCHAR(255) DEFAULT '' NOT NULL COLLATE utf8mb4_unicode_ci	
ALTER TABLE demo CHANGE name demo_name VARCHAR(20) DEFAULT '' NOT NULL

迁移文件的另一种简介执行方式,使用DB statement 执行原生DDL语句

DB::statement("ALTER TABLE `lara`.`users` CHANGE COLUMN `remember_token` `remember_tokens` VARCHAR(255) COLLATE 'utf8mb4_unicode_ci' NULL DEFAULT NULL");

总结

个人感觉这种方式适合中小型项目和公司,在有DBA的公司应该使用专业的数据库迁移工具

Laravel Telescope

参考

简介

Larave Telescope 是 Laravel 框架的官方出品的debug工具包, 5.7.7以上版本才有.

此文用来记录一些文档中没有介绍的坑点,使用技巧以及原理

安装配置参考文档

1.扩展自带数据表迁移文件,配合其它迁移文件扩展包生成时要注意

定制数据迁移

如果您不打算使用 Telescope 的默认迁移,则应该在 AppServiceProvider 的 register 方法中调用 Telescope::ignoreMigrations 方法。您可以使用 php artisan vendor:publish --tag=telescope-migrations 命令导出默认迁移。

注意:如果使用三方扩展包生成迁移文件或手动编写迁移文件时不要生成Telescope相关迁移文件,否则执行时会造成冲突

按钮功能介绍

1暂停 2刷新 3定制Tag 可以对指定Tag数据进行监控

Tag设置规则,Auth:ID 模型:ID 可以看一下 telescope_entries_tags 表中自动记录的标签。但是在环境变量设置为production后,除了Auth其它的并不好使

生产环境通过认证访问

protected function gate()
    {
        Gate::define('viewTelescope', function ($user) {
            return in_array($user->email, [
                'jordon.kub@example.com', 'njerde@example.net'
            ]);
        });
    }

设置制定邮箱或自定以其他字段的用户,前提必须使用laravel自带的用户认证功能

测试问题

参考

启用了telescope(望远镜)debug工具之后,用phpunit 跑所有测试是会报ReflectionException: Class env does not exist 错误

解决方案

You could just add <env name="TELESCOPE_ENABLED" value="false"/> to the phpunit.xml-file.
//将TELESCOPE_ENABLED false添加到phpunit.xml配置中

Laravel Config 缓存原理

参考

加载过程

  • laravel的所有配置文件都在config目录下
  • 启动时加载所有config目录下的所有配置文件
  • 通过Dotenv 类库加载.env文件中的配置项到预定义全局变量$_ENV中
  • env函数中使用getenv()来获取环境变量$_ENV中的值
  • config文件中使用env()加载配置值
  • config()函数调用config文件中的配置项

使用配置文件时要注意严格遵守约定,在config文件中调用env()函数

在路由,控制器和模型以及自定义类文件中必须使用config()函数获取配置项的值

Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
 /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        //此处判断是否开启缓存配置,开启缓存配置直接返回缓存中的配置
        //因此开启缓存配置后,env()函数会失效
        if ($app->configurationIsCached()) {
            return;
        }

        $this->checkForSpecificEnvironmentFile($app);
        //此处加载.env中的文件
        try {
            (new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
        } catch (InvalidPathException $e) {
            //
        }
    }
/**
     * Gets the value of an environment variable.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return mixed
     */
    function env($key, $default = null)
    {
        $value = getenv($key);

        if ($value === false) {
            return value($default);
        }

        switch (strtolower($value)) {
            case 'true':
            case '(true)':
                return true;
            case 'false':
            case '(false)':
                return false;
            case 'empty':
            case '(empty)':
                return '';
            case 'null':
            case '(null)':
                return;
        }

        if (strlen($value) > 1 && Str::startsWith($value, '"') && Str::endsWith($value, '"')) {
            return substr($value, 1, -1);
        }

        return $value;
    }
/**
     * Get / set the specified configuration value.
     *
     * If an array is passed as the key, we will assume you want to set an array of values.
     *
     * @param  array|string  $key
     * @param  mixed  $default
     * @return mixed|\Illuminate\Config\Repository
     */
    function config($key = null, $default = null)
    {
        if (is_null($key)) {
            return app('config');
        }

        if (is_array($key)) {
            return app('config')->set($key);
        }

        return app('config')->get($key, $default);
    }
//app('config) 生成的是 Illuminate\Config\Repository 实例

开启缓存配置

php artisan config:cache

/**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {  
        //调用清楚缓存命令
        $this->call('config:clear');
       //加载所有配置项
        $config = $this->getFreshConfiguration();
       //使用var_export导出php可以执行的数组
       //文件系统使用file_put_contents 生成 配文件 config.php
       //配置文件缓存目录 app/bootstrap/cache/config.php
        $this->files->put(
            $this->laravel->getCachedConfigPath(),
            '<?php return ' . var_export($config, true) . ';' . PHP_EOL
        );

        $this->info('Configuration cached successfully!');
    }

php artisan config:clear

/**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {   
        //laravel文件系统使用 unlink 删除 缓存的config.php文件
        $this->files->delete($this->laravel->getCachedConfigPath());

        $this->info('Configuration cache cleared!');
    }

总结

处理思想就是把多个配置文件合并成一个配置文件,减少I/0操作提升性能.

Laravel框架读写分离测试

#配置修改
 'mysql' => [
            'read' => [
                'host' => '192.168.31.194',
            ],
            'write' => [
                'host' => '127.0.0.1',
            ],
            #同一请求周期内使用相同链接获取数据
            'sticky'    => true,
            'driver'    => 'mysql',
            'database'  => 'lara',
            'username'  => 'root',
            'password'  => '123456',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix'    => '',
  ],

测试代码

public function create()
    {
        dump(Order::count());
        $res = Order::create(['sn' => date('YmdHis') . mt_rand(0000, 9999)]);
        dump(Order::find($res->id), Order::count());
    }

stop slave; 关闭从库同步

sticky 为 false时

sticky 为 true时

写入之后的读取的count数为47 插入的id模型也打印成功,说明laravel在写入操作之后使用的是写连接.

源码分析

Illuminate\Database\Connection.php

/**
     * Get the current PDO connection used for reading.
     *
     * @return \PDO
     */
    public function getReadPdo()
    {
        if ($this->transactions > 0) {
            return $this->getPdo();
        }

        if ($this->getConfig('sticky') && $this->recordsModified) {
            return $this->getPdo();
        }

        if ($this->readPdo instanceof Closure) {
            return $this->readPdo = call_user_func($this->readPdo);
        }

        return $this->readPdo ?: $this->getPdo();
    }

代码判断配置sticky 为true 并且数据修改成功返回当当前链接

Laravel 表单验证解析

laravel表单验证的异常响应

使用控制器的$this->validate($request,[‘body’ => ‘required’,]);或创建的表单请求验证时,laravel会根据ajax请求或Accept:application/json请求头自动响应Json数据,普通请求会自动重定向之前的页面

源码解析

laravel 异常处理类 HandlerIlluminate\Foundation\Exceptions\Handler

/**
 * Create a response object from the given validation exception.
 *
 * @param \Illuminate\Validation\ValidationException $e
 * @param \Illuminate\Http\Request                   $request
 *
 * @return \Symfony\Component\HttpFoundation\Response
 */
protected function convertValidationExceptionToResponse(ValidationException $e, $request)
{

        if ($e->response) {
            return $e->response;
        }

        return $request->expectsJson()
                    ? $this->invalidJson($request, $e)
                    : $this->invalid($request, $e);
}

/**
 * Convert a validation exception into a response.
 *
 * @param \Illuminate\Http\Request                   $request
 * @param \Illuminate\Validation\ValidationException $exception
 *
 * @return \Illuminate\Http\Response
 */
protected function invalid($request, ValidationException $exception)
  {
        $url = $exception->redirectTo ?? url()->previous();

        return redirect($url)
                ->withInput($request->except($this->dontFlash))
                ->withErrors(
                    $exception->errors(),
                    $exception->errorBag
                );
 }

/**
 * Convert a validation exception into a JSON response.
 *
 * @param \Illuminate\Http\Request                   $request
 * @param \Illuminate\Validation\ValidationException $exception
 *
 * @return \Illuminate\Http\JsonResponse
 */
protected function invalidJson($request, ValidationException $exception)
 {
        return response()->json([
            'message' => $exception->getMessage(),
            'errors' => $exception->errors(),
        ], $exception->status);
 }

Trait Illuminate\Http\Concerns\InteractsWithContentTypes

/**
 * Determine if the current request probably expects a JSON response.
 *
 * @return bool
 */
public function expectsJson()
{
     return ($this->ajax() && !$this->pjax()) || $this->wantsJson();
}

/**
 * Determine if the current request is asking for JSON in return.
 *
 * @return bool
 */
public function wantsJson()
{
      $acceptable = $this->getAcceptableContentTypes();

      return isset($acceptable[0]) && Str::contains($acceptable[0], ['/json', '+json']);
}

Handler 类中的convertValidationExceptionToResponse() 方法通过 expectsJson() 判断是否为ajax请求或accept请求头 然后通过invalidJson()将异常响应为json 普通请求,通过invalid()方法响应为重定向

Eloquent ORM 解析入门

参考链接https://learnku.com/docs/laravel/5.5/eloquent/1332

Laravel 的 Eloquent ORM 提供了漂亮、简洁的 ActiveRecord 实现来和数据库交互。每个数据库表都有一个对应的「模型」用来与该表交互。你可以通过模型查询数据表中的数据,并将新记录添加到数据表中。

在开始之前,请确保在 config/database.php 中配置数据库连接。更多关于数据库的配置信息,请查看 文档

艺术,优雅,强大,用过的都说好!

Eloquent 模型约定

数据表名 默认使用模型类的复数形式「蛇形命名」来作为表名 ,否则使用定义的 table属性

protected $connection = 'connection-name';//此模型的连接名称
protected $table = 'my_flights';//表名
 public $timestamps = false; //关闭自己动维护时间戳
 protected $dateFormat = 'U'; //模型日期字段存储格式 U表示unix时间戳
 const CREATED_AT = 'creation_date';  //自定义创建时间字段
 const UPDATED_AT = 'last_update';  //自定义更新时间字段
protected $primaryKey = 'id'; // 定义主键字段
protected $keyType = 'int'; //主键类型
public $incrementing = true; //是否开启auto-incrment 自增属性
 protected $fillable = []; // 可以被批量赋值的属性
 
 protected $guarded = [];  // 不可批量复制的属性
  protected $casts = ['is_admin'=>'boolean'];
  //属性转换配合修改器使用时要保持类型一致
  //转换成数组或json时的可见性
  protected $visible = ['first_name', 'last_name'];//显示
  //方法显示
  $model->makeVisible('attribute')->toArray();
  protected $hidden = ['password'];//隐藏
  //方法隐藏
  $model->makeHidden('attribute')->toArray();
protected $appends = ['is_admin']; //追加到模型数组表单的访问器。
//配合访问器
public function getIsAdminAttribute()
{
   return $this->attributes['admin'] == 'yes';
}
//运行时追加
return $model->append('is_admin')->toArray();

return $model->setAppends(['is_admin'])->toArray();

模型方法

模型的查询构造器中注入了DB的查询构造器,所以模型也支持DB查询构造器的方法.这里只解析模型查询构造器的独有方法或模型和DB的特殊方法

all() 查询所有

$flights = App\Flight::all();//查询所有结果
模型内方法,实际调用的是DB的get方法,参数是字段支持数组和多个参数形式
all方法直接创建了一个查询构造器然后执行get()方法,所以调用all方法之前无法调用任何查询构造器的方法
public static function all($columns = ['*'])
{
     return (new static)->newQuery()->get(
         is_array($columns) ? $columns : func_get_args()
     );
}

cursor() 游标遍历,适用于大数据量处理.


foreach (User::select('id', 'name')->cursor() as $user) {
   //处理操作    
   dump($user);
}

内部实现使用PHP generator 进行遍历 生成器是协程调度, 耗时和消耗内存是恒定的(接近),跟数据量无关.

 /**
  * Get a generator for the given query.
  *
  * @return \Generator
  */
public function cursor()
{
     foreach ($this->applyScopes()->query->cursor() as $record) {
         yield $this->model->newFromBuilder($record);
     }
}

chunk() 分块处理 适用于小数据量快速处理

Flight::chunk(200, function ($flights) {
    foreach ($flights as $flight) {
        //
    }
});

内部实现 do while 循环 执行的分页查询sql 耗时和消耗内存取决分块数量,分块数量越大消耗内存和耗时相应减少

select * from `flights` order by `flights`.`id` asc limit 200 offset 0  
select * from `flights` order by `flights`.`id` asc limit 200 offset 200  
select * from `flights` order by `flights`.`id` asc limit 200 offset 400  

附上测试代码

//内存计算函数
function print_memory_info($msg, $real_usage = false)
{
    echo $msg, ceil(memory_get_usage($real_usage) / 1024), 'KB', '<br>';

    return memory_get_usage($real_usage);
}

//使用游标方式
public function test1(Request $request)
{
        $start_at = microtime(true);
        $start = print_memory_info('cursor开始内存');
        foreach (User::where('id', '<=', '10000')->cursor() as $user) {
            //dump($user->id);
        }
        $end = print_memory_info('cursor结束内存');
        $end_at = microtime(true);

        echo '消耗内存 ',($end - $start) / 1024 .'KB','<br>';
        echo '耗时 ',$end_at - $start_at,'微妙';
}

//使用chunk方式
public function test2(Request $request)
{
        $start_at = microtime(true);
        $start = print_memory_info('Chunk开始内存');
        User::where('id', '<=', '10000')->Chunk(10000, function ($users) {
            foreach ($users as $user) {
                //dump($user->id);
            }
        });
        $end = print_memory_info('Chunk结束内存');
        $end_at = microtime(true);
        echo '消耗内存 ', ($end - $start) / 1024 .'KB', '<br>';
        echo '耗时 ', $end_at - $start_at, '微妙';
}
测试结果

firstOrNew() 和 firstOrCreate() 和firstOrUpdate()

//传参支持两种方式 两个数组或一个数组 查询和写入的数据匹配为传入的数组
$user = User::firstOrNew(['name' => 'Favian Orn'], ['email' => 'max.koss@example.com']);

firstOrnew 返回的模型还尚未保存到数据库,必须要手动调用 save 方法才能保存它
如果想使用批量赋值,需要先调用fill方法
$user->fill(['name' => '1234'])->save();

$user = User::firstOrCreate(['name' => 'Favian Orn', 'email' => 'max.koss@example.com']);
$user->update(['name' => '1234']);

$user = User::firstOrUpdate(['name' => 'Favian Orn', 'email' => 'max.koss@example.com']);

Larave Horizon 队列仪表盘

安装使用参考文档

https://learnku.com/docs/laravel/5.5/horizon/1345#configuration

本文主要记录一些使用中碰到的问题,文档有的不再赘述.

配置文件介绍

 /*
    |--------------------------------------------------------------------------
    | Horizon Redis Connection
    |--------------------------------------------------------------------------
    |
    | This is the name of the Redis connection where Horizon will store the
    | meta information required for it to function. It includes the list
    | of supervisors, failed jobs, job metrics, and other information.
    |
    */

    'use' => 'payhandle',

指定horizon 使用的redis连接,连接配置可在config/database.php中增加

这是Horizon将存储的Redis连接的名称,它运作所需的元信息。 它包括清单主管,失业,工作指标和其他信息。

/*
    |--------------------------------------------------------------------------
    | Horizon Redis Prefix
    |--------------------------------------------------------------------------
    |
    | This prefix will be used when storing all Horizon data in Redis. You
    | may modify the prefix when you are running multiple installations
    | of Horizon on the same server so that they don't have problems.
    |
    */

    'prefix' => env('HORIZON_PREFIX', 'horizon:'),

horizon 在redis中使用的存储前缀, 使用同一名称时,可以负载均衡水平扩展,不会重复处理队列中的任务.

在Redis中存储所有Horizon数据时将使用此前缀。 您可以在同一台服务器运行多个安装horizon时修改前缀,以便它们没有问题。

/*
    |--------------------------------------------------------------------------
    | Horizon Route Middleware
    |--------------------------------------------------------------------------
    |
    | These middleware will get attached onto each Horizon route, giving you
    | the chance to add your own middleware to this list or change any of
    | the existing middleware. Or, you can simply stick with this list.
    |
    */

    'middleware' => ['web'],

Horizon设置的路由中间件

这些中间件将附加到每个Horizon路由上,为您提供机会将您自己的中间件添加到此列表或更改任何现有的中间件

/*
    |--------------------------------------------------------------------------
    | Queue Wait Time Thresholds
    |--------------------------------------------------------------------------
    |
    | This option allows you to configure when the LongWaitDetected event
    | will be fired. Every connection / queue combination may have its
    | own, unique threshold (in seconds) before this event is fired.
    |
    */

    'waits' => [
        'redis:default' => 60,
    ],

队列等待时间阈值

此选项允许您配置LongWaitDetected事件将被解雇的时间。 每个连接/队列组合都可以拥有它,触发此事件之前的唯一阈值(以秒为单位)。

/*
    |--------------------------------------------------------------------------
    | Job Trimming Times
    |--------------------------------------------------------------------------
    |
    | Here you can configure for how long (in minutes) you desire Horizon to
    | persist the recent and failed jobs. Typically, recent jobs are kept
    | for one hour while all failed jobs are stored for an entire week.
    |
    */

    'trim' => [
        'recent' => 60,
        'failed' => 10080,
    ],

队列清空时间

在这里,您可以配置Horizon需要多长时间(以分钟为单位)坚持最近和失败的工作。 通常,保留最近的工作 一个小时,所有失败的工作都存储了整整一个星期。

/*
    |--------------------------------------------------------------------------
    | Fast Termination
    |--------------------------------------------------------------------------
    |
    | When this option is enabled, Horizon's "terminate" command will not
    | wait on all of the workers to terminate unless the --wait option
    | is provided. Fast termination can shorten deployment delay by
    | allowing a new instance of Horizon to start while the last
    | instance will continue to terminate each of its workers.
    |
    */

    'fast_termination' => false,

快速终止

当这个选项被启用.Horizon的 terminate 命令 将不会等待,直到所有的worker(进程)终止后才终止, 除非提供了–wait选项 .快速终止可以缩短部署延时时间,允许新的 Horizon实例在最后一个实例启动时,终止他的所有worker(进程)

/*
    |--------------------------------------------------------------------------
    | Queue Worker Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may define the queue worker settings used by your application
    | in all environments. These supervisors and settings handle all your
    | queued jobs and will be provisioned by Horizon during deployment.
    |
    */

    'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 3,
            ],
        ],

        'local' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 3,
                'tries' => 3,
            ],
        ],
    ],

队列worker(进程)配置

这里可以定义你应用程序在所有的环境中,使用的队列 worker(进程)的设置. 这些 supervisor 和 配置 处理你所有的队列任务 ,在部署期间将由Horizon提供

Redis超时问题

https://blog.minhow.com/2018/01/14/laravel/predis-error-reading/

1.修改 Laravel 的 config/database.php 配置文件,找到 redis 的配置项,加上 read_write_timeout 参数:
'redis' => [
        'cluster' => false,
        'default' => [
            'host' => env('REDIS_HOST', 'localhost'),
            'password' => env('REDIS_PASSWORD', null),
            'port' => env('REDIS_PORT', 6379),
            'database' => 0,
            'read_write_timeout' => 0//增加此设置
        ],
],
2.修改 Redis 的 redis.conf 配置文件,找到 timeout 参数,改成0
 timeout=0

Horizon 使用 Redis Cluster (集群)

https://github.com/laravel/horizon/issues/274

config/database.php

 'redis' => [

        'client' => env('REDIS_CLIENT', 'predis'),

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'predis'),
            'prefix' => Str::slug(env('APP_NAME', 'laravel'), '_') . '_database_',
        ],

        'clusters' => [
            'default' => [
                [
                    'host' => env('REDIS_HOST', 'localhost'),
                    'password' => env('REDIS_PASSWORD', null),
                    'port' => env('REDIS_PORT', 6379),
                    'database' => 0,
                    'read_write_timeout' => 0
                ],
            ],
            'cache' => [
                [
                    'host' => env('REDIS_HOST', '127.0.0.1'),
                    'password' => env('REDIS_PASSWORD', null),
                    'port' => env('REDIS_PORT', 6379),
                    'database' => env('REDIS_CACHE_DB', 1),
                    'read_write_timeout' => 0
                ]
            ],
            'horizon' => [
                [
                    'host' => env('REDIS_HOST', 'localhost'),
                    'password' => env('REDIS_PASSWORD', null),
                    'port' => env('REDIS_PORT', 6379),
                    'database' => 2,
                    'read_write_timeout' => 0
                ],
            ],
        ],

config/horizon.php

 'use' => env('HORIZON_USE', 'clusters.default'),

最关键的配置

Redis 集群

如果你的 Redis 队列驱动使用了 Redis 集群,你的队列名必须包含一个 key hash tag 。这是为了确保所有的 Redis 键对于一个队列都被放在同一哈希中。

key hash tag 介绍 https://learnku.com/articles/25070

config/horizon.php 需要给队列名称加{},laravel是以队列名作为redis的key

'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['{default}'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 3,
            ],
        ],

        'local' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['{default}'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 3,
            ],
        ],
    ],

如果使用queue队列,则修改config/queue.php,给队列名称加{}

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => '{default}',
    'retry_after' => 90,
],

Horizon不适用与redis集群

https://github.com/laravel/horizon/issues/274

horizon自定义队列配置

参考 https://github.com/laravel/horizon/issues/255

需求是要将队列的redis和和缓存的redis区分开,使用不同的redis。

目前尝试,默认队列一定要有,否则Job使用默认队列分发时,队列会出现不执行的情况。所以解决方案是修改config/queue.php中redis的链接来实现。

config/queue.php文件
 
'redis' => [
            'driver' => 'redis',
            'connection' => 'horizon',//修改队列连接
            'queue' => env('REDIS_QUEUE', 'default'),
            'retry_after' => 90,
            'block_for' => null,
 ],
  
//此处也可以增加自动定义队列
'horizon' => [
            'driver' => 'redis',
            'connection' => 'horizon',
            'queue' => env('HORIZON_REDIS_QUEUE', 'horizon'),
            'retry_after' => 1800,
            'block_for' => null,
],
config/horizon.php文件
默认队列要保留
'environments' => [
        'production' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 3,
            ],
    //此处对应horizon队列
            'horizon' => [
                'connection' => 'horizon',
                'queue' => ['horizon'],
                'balance' => 'simple',
                'processes' => 10,
                'tries' => 3,
                'timeout' => 0,
            ],
        ],

        'local' => [
            'supervisor-1' => [
                'connection' => 'redis',
                'queue' => ['default'],
                'balance' => 'simple',
                'processes' => 3,
                'tries' => 3,
            ],
 //此处对应horizon队列
            'horizon' => [
                'connection' => 'horizon',
                'queue' => ['horizon'],
                'balance' => 'simple',
                'processes' => 3,
                'tries' => 3,
                'timeout' => 0,
            ],
        ],
    ],