runFill([ 'xhGhsOrder' => GhsOrder::tableName(), 'xhCg' => Purchase::tableName(), ]); } /** * 仅补充 xhGhsOrder */ public function actionFillGhsOrder() { return $this->runFill([ 'xhGhsOrder' => GhsOrder::tableName(), ]); } /** * 仅补充 xhCg */ public function actionFillCg() { return $this->runFill([ 'xhCg' => Purchase::tableName(), ]); } private function runFill(array $tables) { ini_set('memory_limit', '512M'); set_time_limit(0); if ($this->batchSize < 1) { $this->stderr("batchSize 必须大于 0\n"); return ExitCode::UNSPECIFIED_ERROR; } if ($this->scanSize < 1) { $this->stderr("scanSize 必须大于 0\n"); return ExitCode::UNSPECIFIED_ERROR; } if (!in_array($this->mode, ['empty', 'pk'], true)) { $this->stderr("mode 仅支持 empty 或 pk\n"); return ExitCode::UNSPECIFIED_ERROR; } $exitCode = ExitCode::OK; foreach ($tables as $label => $tableName) { $code = $this->fillTable($label, $tableName); if ($code !== ExitCode::OK) { $exitCode = $code; } } return $exitCode; } private function fillTable($label, $tableName) { $db = Yii::$app->db; $db->createCommand('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')->execute(); $this->stdout("{$label}({$tableName}) 开始补充 salt,模式: {$this->mode}\n"); if ($this->mode === 'empty') { $processed = $this->fillByEmptySalt($label, $tableName); } else { $processed = $this->fillByPrimaryKeyScan($label, $tableName); } $this->stdout("{$label} 完成,本次处理 {$processed} 条\n"); return ExitCode::OK; } /** * 只查 salt='' 的记录,补完即停,不会扫描已有 salt 的上千万数据 */ private function fillByEmptySalt($label, $tableName) { $db = Yii::$app->db; $processed = 0; $lastId = 0; while (true) { if ($this->limit > 0 && $processed >= $this->limit) { break; } $batchLimit = $this->limit > 0 ? min($this->batchSize, $this->limit - $processed) : $this->batchSize; $rows = $db->createCommand( "SELECT id FROM {$tableName} WHERE salt = '' AND id > :lastId ORDER BY id ASC LIMIT {$batchLimit}" )->bindValue(':lastId', $lastId)->queryColumn(); if (empty($rows)) { break; } $processed += $this->updateIds($label, $tableName, $rows); $lastId = (int)end($rows); $this->stdout("{$label} 已处理 {$processed} 条,当前 id <= {$lastId}\n"); $this->sleepBetweenBatch(); } return $processed; } /** * 按主键游标分段扫描,查询只走主键索引;适合 empty 模式执行计划不佳时使用 */ private function fillByPrimaryKeyScan($label, $tableName) { $db = Yii::$app->db; $processed = 0; $lastId = 0; while (true) { if ($this->limit > 0 && $processed >= $this->limit) { break; } $ids = $db->createCommand( "SELECT id FROM {$tableName} WHERE id > :lastId ORDER BY id ASC LIMIT {$this->scanSize}" )->bindValue(':lastId', $lastId)->queryColumn(); if (empty($ids)) { break; } $lastId = (int)end($ids); $minId = (int)$ids[0]; $maxId = $lastId; $emptyIds = $db->createCommand( "SELECT id FROM {$tableName} WHERE id >= :minId AND id <= :maxId AND salt = '' ORDER BY id ASC" )->bindValues([ ':minId' => $minId, ':maxId' => $maxId, ])->queryColumn(); if (!empty($emptyIds)) { if ($this->limit > 0) { $emptyIds = array_slice($emptyIds, 0, $this->limit - $processed); } $processed += $this->updateIds($label, $tableName, $emptyIds); } $this->stdout("{$label} 已处理 {$processed} 条,已扫描到 id={$lastId}\n"); $this->sleepBetweenBatch(); } return $processed; } private function updateIds($label, $tableName, array $ids) { $db = Yii::$app->db; $updated = 0; foreach ($ids as $id) { $id = (int)$id; if ($id <= 0) { continue; } if ($this->dryRun) { $this->stdout("[dry-run] {$label} id={$id}\n"); $updated++; continue; } $salt = stringUtil::charsShuffleLowerCase(10); $affected = $db->createCommand( "UPDATE {$tableName} SET salt = :salt WHERE id = :id AND salt = ''" )->bindValues([ ':salt' => $salt, ':id' => $id, ])->execute(); if ($affected > 0) { $updated++; } } return $updated; } private function sleepBetweenBatch() { if ($this->sleepMs > 0) { usleep($this->sleepMs * 1000); } } }