【问题标题】:Update about 1 million rows in MySQL table every 1 hour每 1 小时更新 MySQL 表中大约 100 万行
【发布时间】:2020-07-21 08:35:58
【问题描述】:

我使用 Codeigniter 3.1.11 并有一个问题。我需要每 1 小时通过 Cron 更新 MySQL 表中的大约 100 万行(将来会更多)。但问题是,如果我使用此代码更新超过约 200-300 行,我的服务器 CPU 已加载 100%,并且表在约 200-300 行后停止更新。而且我什至必须重新启动服务器上的 PHP 才能使服务器恢复正常。

我做错了什么?

如何正确地完成这个任务,以便快速执行对数据库的查询,并且不会对服务器造成太大的负载。

这是来自控制器的代码:

function cron_rows_update() {
$this->members_model->rows_update();
} 

这是来自模型的代码:

function rows_update() {
   $currency_numbers_after_dot = $this->currencies_model->get_currency('ONE', 'My currencty')['numbers_after_dot'];
   $game_currency_percentage_max = $this->settings_model->get_settings_details('game_rating_percentage_max')['value'];
   $game_currency_speed_in_hour = $this->settings_model->get_settings_details('game_currency_speed_in_hour')['value'];
   $this->db->from('members');
   $query = $this->db->get();
   if($query->num_rows() > 0) {
    foreach($query->result_array() as $row) {
        $game_total_balance = round($row['game_vault_balance'] + $row['game_available_balance'], $currency_numbers_after_dot);

        // Game Rating Calculate
        // Rating Part1
        // Rating Part1_1
        if ($row['game_vault_balance'] == '0') {
            $rating_part1_1 = '0'; 
        }
        if ($row['game_vault_balance'] > '0' AND $row['game_vault_balance'] < '20') {
            $rating_part1_1 = '0.1'; 
        }
        if ($row['game_vault_balance'] > '20' 
            AND $row['game_vault_balance'] < '2000') {
        
            $max_game_vault_balance = '2000';
            $percent = floor($row['game_vault_balance'] * 100 / $max_game_vault_balance);
            $additional_rating = '0.05' * $percent / 100;
               
            $rating_part1_1 = round('0.1' + $additional_rating, 2); 
    
        }
        if ($row['game_vault_balance'] >= '2000') {
            $rating_part1_1 = '0.15'; 
        }
   
        // Rating Part1_2
        if ($game_total_balance == '0') {
            $PER_part1_2 = '0'; 
        }
        if ($game_total_balance > '0' AND $game_total_balance < '20') {
            $rating_part1_2 = '0.1'; 
        }
        if ($game_total_balance > '20' AND $game_total_balance < '2000') {
            $max_game_total_balance = '2000';
            $percent = floor($game_total_balance * 100 / $max_game_total_balance);
            $additional_rating = '0.05' * $percent / 100;
            $rating_part1_2 = round('0.1' + $additional_rating, 2); 
        }
        if ($game_total_balance >= '2000') {
            $rating_part1_2 = '0.15';
        }
        // Rating part1_3
        $rating_part1_3 = '0';
        // Rating part1_4
        $PER_part1_4 = '0';
        // Rating part2
        $PER_part2 = '0';
        // Rating part3
        $PER_part3 = '0';
        // Calculate all rating
        $rating = round($rating_part1_1 + $rating_part1_2 + $rating_part1_3 + $rating_part1_4 + $rating_part2 + $rating_part3, 2);
   
        if ($rating <= '1') {
            $rating_member = $rating;
        }
        if ($rating > '1') {
            $rating_member = floor($rating);
        }
        // Game balance calculate
        $amount_from_game_vault_in_hour = $game_currency_speed_in_hour / '100' * $row['game_vault_balance'];
        $new_balance_in_hour = ($game_currency_percentage_max / '100') * $row['rating'] * $amount_from_game_vault_in_hour;
        $game_balance_in_hour_amount = $amount_from_game_vault_in_hour + $new_balance_in_hour;

        // Update row in members table

        if ($game_total_balance > '0') {
            $this->db->where("UserID", $row['UserID']);
            $this->db->set("game_vault_balance", "game_vault_balance - " . $amount_from_game_vault_in_hour, FALSE);
            $this->db->set("game_available_balance", "game_available_balance + " . $game_balance_in_hour_amount, FALSE);
            $this->db->set("rating", $rating_member, FALSE);
            $this->db->set("game_rating_and_balance_update_last_time", 'NOW()', FALSE);
            $this->db->update("members");
        }
    } 
}
return;    
}

【问题讨论】:

  • 如果服务器在 200 或 300 个查询时出现故障,那么您应该增加 server 容量或查询数量。
  • 检查您的服务器错误日志。分享您的服务器配置,我们或许可以为您提供帮助。
  • @BasheerKharoti 我尝试将我的服务器扩展到 32GB RAM 和 8 个 CPU,结果是一样的。我想我可能不应该使用 foreach 并且需要其他方法来加载我的服务器很少?
  • 使用 shuf 可能吗?
  • @Hack5 你是什么意思?在哪里使用 shuf?

标签: php mysql codeigniter codeigniter-3


【解决方案1】:

尝试限制更新而不是一次全部更新。分块更新。

function cron_rows_update() {
   $num_of_members = $this->members_model->get_num_of_members();
   $limit_per_run = 150;
   $total_run = (int)$num_of_members/$per_run;
   
   for( $i = 0; $i <= $total_run; $i++ ) {
      $offset = $i * $limit_per_run;
      $this->members_model->rows_update($offset, $limit_per_run);
   }

}

rows_update()

function rows_update($offset, $limit_per_run) {
    
    **

    $this->db->limit($offset, $limit_per_run);
    $query = $this->db->get();
    
    $arrUpdateBatchData = [];
    
    while ($row = $query->unbuffered_row('array'))
    {
        // calculations and create array for batch update
    }

    // update in batch limiting to selected number of records
    if (count($arrUpdateBatchData) > 0)
    {
        $this->db->update_batch('members', $arrUpdateBatchData, 'UserID');
    }
}

您也可以尝试使用控制器中的计时器功能,在一定时间间隔后触发更新,并增加服务器容量,如 cmets 中所述。

阅读更多here 以获取有关尽可能快地进行大量记录更新并减少服务器负载的更多说明。

【讨论】:

  • 谢谢!我会试着写下结果。但是您认为这是一个临时解决方案,或者它是否适用于“成员”表中超过 100 万行的情况?或者我需要搜索“foreach”以外的其他方法?
  • 我认为您应该从各个方面进行优化.. 优化代码(php 逻辑和 sql 查询)、数据库配置和服务器配置以供将来运行,您必须更新数千条记录以更快地执行和服务器负载低
  • 谢谢。你觉得在这种情况下,我应该在表列上添加索引,应该添加哪些索引?
  • 如果我在我的代码中使用 foreach($query->result_array() as $row) 我应该在 MySQL 中使用索引吗?
  • 主键和外键将被自动索引,这在使用 ID 搜索时非常有用。当在大型数据库中搜索 ID 以外的任何其他字段时,索引该字段将大大改善获取数据。 . $this-&gt;db-&gt;get() 返回您正在执行其他操作的结果。返回结果后,foreach() 将不再重要。
【解决方案2】:

除了您的代码有点混乱($PER_part2$PER_part3 未使用)之外,我会执行以下操作:

对于超过 100 万次迭代,该脚本将无法与 result_array 一起使用。 原因是,result_array 将所有数据存储在一个数组中 - 迟早你会遇到内存限制问题。

为了避免这种情况,您必须使用unbuffered_row。 此方法返回单个结果行,而不会将整个结果预取到内存中。

查看他们的文档here。 (部分 unbuffered_row)

接下来我要改变的是你的 if 块——我会在这里使用if/else 语法。 (差别不大,但考虑到您的行数 - 它可能会有所帮助)

另外,我将您计算评分的模块外包了两次,背后的逻辑相同。

基本上以下应该可以工作:

function rows_update() 
{
    $currency_numbers_after_dot = $this->currencies_model->get_currency('ONE', 'My currencty')['numbers_after_dot'];
    $game_currency_percentage_max = $this->settings_model->get_settings_details('game_rating_percentage_max')['value'];
    $game_currency_speed_in_hour = $this->settings_model->get_settings_details('game_currency_speed_in_hour')['value'];
    $this->db->from('members');
    $query = $this->db->get();
    
    $arrUpdateBatchData = [];
    
    while ($row = $query->unbuffered_row('array'))
    {
        $game_total_balance = round($row['game_vault_balance'] + $row['game_available_balance'], $currency_numbers_after_dot);
        if ($game_total_balance > 0) {
            // Game Rating Calculate
            // Rating Part1
            // Rating Part1_1
            $rating_part1_1 = $this->getRatingPart($row['game_vault_balance']);
            $rating_part1_2 = $this->getRatingPart($game_total_balance);
            // Rating part1_3
            $rating_part1_3 = 0;
            // Rating part1_4
            $rating_part1_4 = 0;
            // Rating part2
            $PER_part2 = '0';
            // Rating part3
            $PER_part3 = '0';
            // Calculate all rating
            $rating = round($rating_part1_1 + $rating_part1_2 + $rating_part1_3 + $rating_part1_4 + $rating_part2 + $rating_part3, 2);
            if ($rating <= 1) {
                $rating_member = $rating;
            }
            elseif ($rating > 1) {
                $rating_member = floor($rating);
            }
            
            // Game balance calculate
            $amount_from_game_vault_in_hour = $game_currency_speed_in_hour / '100' * $row['game_vault_balance'];
            $new_balance_in_hour = ($game_currency_percentage_max / '100') * $row['rating'] * $amount_from_game_vault_in_hour;
            $game_balance_in_hour_amount = $amount_from_game_vault_in_hour + $new_balance_in_hour;
            
            $arrUpdateData = [
                'UserID' => $row['UserID'], 
                'game_vault_balance' => ($row['game_vault_balance'] - $amount_from_game_vault_in_hour),
                'game_available_balance' => ($row['game_available_balance'] - $game_balance_in_hour_amount),
                'rating' => $rating_member,
                'game_rating_and_balance_update_last_time' => date('Y-m-d H:i:s')
            ];
            
            $arrUpdateBatchData[] = $arrUpdateData;
            
        }
        
        if (count($arrUpdateBatchData) > 500)
        {
            $this->db->update_batch('members', $arrUpdateBatchData, 'UserID');
            $arrUpdateBatchData = [];
        }       
    }
    
    //update last items
    if (count($arrUpdateBatchData) > 0)
    {
        $this->db->update_batch('members', $arrUpdateBatchData, 'UserID');
        $arrUpdateBatchData = [];
    }
    return;    
}

function getRatingPart($val)
{
    if ($val == 0) {
        $rating_part = 0; 
    }
    elseif ($val > 0 AND $val < 20) 
    {
        $rating_part = '0.1'; 
    }
    elseif ($val > 20 AND $val < 2000) 
    {
    
        $max_game_vault_balance = 2000;
        $percent = floor($val * 100 / $max_game_vault_balance);
        $additional_rating = 0.05 * $percent / 100;
           
        $rating_part = round(0.1 + $additional_rating, 2); 

    }
    else {
        $rating_part = 0.15; 
    }
    
    return $rating_part;
}

【讨论】:

  • 谢谢!使用update_batch 是个好主意。现在它更新 37000 行 9 秒。但是如何在这个查询中使用索引来减少请求时间呢?我可以使用例如$this-&gt;db-&gt;update_batch('members use index (UserID) ', $arrUpdateBatchData, 'UserID');吗?
  • 我认为您应该为此提出另一个问题。为此需要包括创建查询在内的 SQL 查询。
猜你喜欢
  • 2021-11-14
  • 2021-11-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-02
  • 1970-01-01
  • 1970-01-01
  • 2015-03-04
相关资源
最近更新 更多