命令执行时超时

Command times out when executing

Laravel version:
8.x

PHP version:
8.0

MySQL version:
8.0

Server:
Larave Forge

我是 运行 我网站上的一个命令

php artisan command:here

几分钟后我得到这个状态“超时”,这里有什么可以优化以防止超时吗?

subscriptions table 有 40,000 条记录,incomes table 有 8,000,000 条记录。每个订阅在 incomes table 中最多有 200 条记录。

为了解释这个命令的作用,这是修复(通过插入)每个 ACTIVE 订阅的收入缺失,这将根据与上次插入的收入的小时差来识别。每个订阅在 incomes table 中最多有 200 条记录,因此如果脚本检测到 subscription 已经达到 200 条收入记录,它将 status 更新为 COMPLETED.

Subscription.php(型号)

public function latestIncome()
{
    return $this->hasOne(Income::class)->latestOfMany();
}

Income.php(型号)

public function subscription()
{
    return $this->belongsTo(Subscription::class);
}
namespace App\Console\Commands;

class SomeCommand extends Command
{
    protected $signature = 'command:here';

    public function handle()
    {
        ini_set('max_execution_time', 0);

        foreach (Subscription::with('latestIncome')->withCount('income')->where('status', 'ACTIVE')->lazy() as $subscription) {
            $count_earnings = $subscription->income_count;
            $recent_bonus = $subscription->latestIncome;

            if ($recent_bonus) {
                if ($count_earnings < 200) {
                    $hour_difference = now()->diffInHours($recent_bonus->created_at);

                    if ($hour_difference > 1) {
                        $to_insert = 200 - $count_earnings;
                        $max = $hour_difference;

                        if ($hour_difference > $to_insert) {
                            $max = $to_insert;
                        }

                        for ($i = 0; $i < $max; $i++) {
                            $income = new Income;
                            $income->user_id = $subscription->user_id;
                            $income->subscription_id = $subscription->id;
                            $income->amount = (100 * 0.002) * 100;
                            $income->save();
                        }

                        if (($count_earnings + $max) >= 200) {
                            $subscription->update(['status' => 'COMPLETED']);
                        }

                        Log::info('Fix for:'.$subscription->id.' | User:'.$subscription->user_id.' | Total:'.$max);
                    }
                } else {
                    $subscription->update(['status' => 'COMPLETED']);
                }
            }
        }
    }
}

您可以在 __construct 函数或您的索引控制器中定义一个时间限制,如果您想要一个较大的时间限制。

    public function __construct()
    {
        set_time_limit(8000000);
    }

如果您批量更新,您可能会获得一些性能:

namespace App\Console\Commands;

class SomeCommand extends Command
{
    protected $signature = 'command:here';

    public function handle()
    {
        ini_set('max_execution_time', 0);

        // Update everything you can before the loop
        Subscription::has('income', '>=', 200)
                ->where('status', '!=' 'COMPLETED')
                ->update([ 'status' => 'COMPLETED' ]);

        foreach (Subscription::with('latestIncome')->withCount('income')->where('status', 'ACTIVE')->lazy() as $subscription) {
            $count_earnings = $subscription->income_count;
            $recent_bonus = $subscription->latestIncome;

            if ($recent_bonus) {
              $hour_difference = now()->diffInHours($recent_bonus->created_at);

              if ($hour_difference > 1) {
                   $to_insert = 200 - $count_earnings;
                   $max = $hour_difference;

                   if ($hour_difference > $to_insert) {
                       $max = $to_insert;
                   }
                        
                   // Some performance gain here possibly
                   Income::insert(collect()->pad($max, [ 
                         'user_id' => $subscription->user_id,
                         'subscription_id' => $subscription->id,
                         'amount' => (100 * 0.002) * 100,
                   ])->all());

                   Log::info('Fix for:'.$subscription->id.' | User:'.$subscription->user_id.' | Total:'.$max);
                }
            }
        }
       // Update anything that got pushed over that threshold
       Subscription::has('income', '>=', 200)
            ->where('status', '!=' 'COMPLETED')
            ->update([ 'status' => 'COMPLETED' ]);
    }
}

  • 我认为子查询是主要的滞后部分。

  • 这不是“sargable”:AND Date(created_at) >= '2022-02-04'。如果可能的话,说

    AND created_at >= '2022-02-04'
    AND created_at  < '2022-02-04' + INTERVAL 1 DAY
    
  • 在上面之后,subscriptions 上的这个索引将是有益的:

    INDEX(status, deleted_at, created_at)
    
  • 哦,我发现这个隐藏在评论中;你想要那个优化吗?

    update  `subscriptions`
         set status = COMPLETED, 
             subscriptions.updated_at = 2022-02-14 17:53:10
      where  
      (
          SELECT  count(*)
              from  incomes
              where  subscriptions.id = incomes.subscription_id
                and  incomes.deleted_at is null
      ) >= 200
        and  status != COMPLETED
        and  subscriptions.deleted_at is null)
    
  • 可能受益于这些索引:

    subscriptions:  INDEX(deleted_at, status)
    incomes:  INDEX(subscription_id, deleted_at)
    
  • 这行不通:updated_at = 2022-02-14 17:53:10;日期时间需要引号。您可以考虑使用 NOW() 而不是构建字符串。

  • 更重要的是,当超过 200 个时,Update 将一次检查 200+ 个中的每一个并更新它们 -- 每个 200+ 次!!努力将 UPDATE 重新排列为“Multitable”更新,其中一个 table 是一个子查询,如下所示:

      SELECT subscription_id
          FROM incomes
          WHERE deleted_at IS NULL
          GROUP BY subscription_id
          HAVING COUNT(*) >= 200
    

这应该会使更新 运行 快一百倍。