線上環境突發「查詢極慢、數據加載不出」,測試環境卻正常。經排查發現 ShopNC 框架 Model 底層主鍵緩存機制失效,導致**每次查詢都全量掃描 INFORMATION_SCHEMA 獲取所有表主鍵**,數據庫負載飆升。本文深度剖析原始碼、失效場景、影響範圍與修復方案。
SHOW FULL PROCESSLIST 大量 SELECT ... FROM INFORMATION_SCHEMA.COLUMNS ... 併發執行。public function tableInfo($table) {
if (empty($table)) return false;
// 1. 嘗試讀取主鍵緩存文件
if (file_exists(BASE_DATA_PATH . '/cache/fields/_pk.php')) {
$this->fields = require(BASE_DATA_PATH . '/cache/fields/_pk.php');
} else {
// 2. 緩存不存在:全量掃描所有表獲取主鍵 (災難現場)
$full_table = Db::showTables(); // SHOW TABLES
$_pk_array = array();
$count = strlen(C('tablepre')); // 表前綴長度
foreach ($full_table as $v_table) {
$v = array_values($v_table);
// 過濾非本應用表前綴
if (substr($v[0], 0, $count) != C('tablepre')) continue;
$tb = str_replace(C('tablepre'), '', $v[0]);
// 逐表查詢結構 (SHOW COLUMNS / DESC)
$fields = DB::showColumns($tb);
foreach ((array) $fields as $k => $v) {
if ($v['primary']) {
$_pk_array[$tb] = $k; // 記錄主鍵欄位名
break;
}
}
}
$this->fields = $_pk_array;
// 3. 寫入緩存文件
F('_pk', $_pk_array, 'cache/fields'); // ShopNC F() 快取函數
}
return $this->fields[$table];
}
F('_pk', ...) 寫入失敗 (目錄無寫入權限、磁碟滿、open_basedir 限制、文件鎖衝突)。require 失敗回傳 false,進入 else 分支。find()、where('id', ...)、save()、delete()、關聯查詢、模型實例化。SHOW TABLES + N × SHOW COLUMNS (N=表數量,典型 100-500 表)。高併發下極易拖垮 MySQL。chown -R www:www /path/to/shopnc/data/cache/
chmod -R 755 /path/to/shopnc/data/cache/
# 部署腳本中排除 fields 目錄
rsync -av --exclude='data/cache/fields/' source/ target/
# 或部署後立即預熱緩存
php -r "require 'init.php'; $model = new Model(); $model->tableInfo('any_table');"
public function tableInfo($table) {
if (empty($table)) return false;
$cacheFile = BASE_DATA_PATH . '/cache/fields/_pk.php';
// 嘗試讀取緩存
if (file_exists($cacheFile)) {
$cached = @require($cacheFile); // @ 抑制錯誤
if (is_array($cached) && isset($cached[$table])) {
return $cached[$table];
}
// 緩存損壞/不含目標表:記錄日誌,不直接全量掃描
error_log("[Model] PK cache corrupted or missing table '$table', rebuilding...");
}
// 雙重檢查鎖模式防併發重建 (需引入文件鎖或 Redis 分布式鎖)
// 簡化版:單機文件鎖
$lockFile = $cacheFile . '.lock';
$fp = fopen($lockFile, 'w');
if (flock($fp, LOCK_EX | LOCK_NB)) {
try {
// 再次檢查 (雙重檢查)
if (file_exists($cacheFile)) {
$cached = @require($cacheFile);
if (is_array($cached) && isset($cached[$table])) return $cached[$table];
}
// 真正需要重建
$this->rebuildPkCache($cacheFile);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
} else {
// 未獲取鎖:等待短暫時間後重試讀取
usleep(100000); // 100ms
if (file_exists($cacheFile)) {
$cached = @require($cacheFile);
if (is_array($cached)) return $cached[$table];
}
}
// 降級:單表查詢 (不全量掃描)
return $this->getSingleTablePk($table);
}
private function rebuildPkCache($cacheFile) {
$full_table = Db::showTables();
$_pk_array = array();
$count = strlen(C('tablepre'));
foreach ($full_table as $v_table) {
$v = array_values($v_table);
if (substr($v[0], 0, $count) != C('tablepre')) continue;
$tb = str_replace(C('tablepre'), '', $v[0]);
$fields = DB::showColumns($tb);
foreach ((array) $fields as $k => $v) {
if ($v['primary']) { $_pk_array[$tb] = $k; break; }
}
}
// 原子寫入:寫臨時文件再 rename
$tmpFile = $cacheFile . '.tmp';
file_put_contents($tmpFile, " $v) {
if ($v['primary']) return $k;
}
return false; // 無主鍵
}
shopnc:pk:{db_name} → Hash (table → pk_column)。M('Table'),未閱讀 Model 基類原始碼。定期閱讀核心框架源碼,是高級工程師必修功課。一個 _pk.php 緩存文件失效,拖垮整個線上數據庫。ShopNC 此坑揭示:**框架便利性背後隱藏著對底層資源極度敏感的假設**。唯有深入原始碼、建立防禦性編程習慣、完善監控告警 (緩存命中率、INFORMATION_SCHEMA 查詢頻率)、定期演練故障注入,才能駕馭複雜系統穩定運行。