經典錯誤:Warning: mysql_query(): Access denied for user 'ODBC'@'localhost' (using password: NO)。根因通常是 重複實例化導致連線關閉、未檢查連線狀態、使用已棄用 `mysql_*` 函數。本文解析錯誤機制、提供單例連線管理、PDO 遷移完整範例、連線池最佳實踐。
<?php
class MysqlClass {
public $link;
public function __construct($host='localhost',$user='root',$pw='',$db='mysql') {
$this->link = mysql_connect($host,$user,$pw);
mysql_select_db($db);
}
public function query($sql){ return mysql_query($sql); }
public function __destruct(){ mysql_close($this->link); }
}
$db = new MysqlClass();
$db = new MysqlClass(); // 第二次建構 -> __destruct 關閉第一個連線資源
$db->query("SELECT * FROM user"); // $this->link 已失效,PHP 回退 ODBC 預設使用者
?>
流程:第一次 `new` 建立連線資源 `#1`;第二次 `new` 建立 `#2`,但指派給同一變數 `$db`,導致 `#1` 失去參考,觸發 `__destruct` 關閉 `#1`;實際上 `$db->link` 指向 `#2`,但若 `#2` 連線失敗 (參數錯誤) 則為 `false`,`mysql_query` 無有效連線,改用預設 ODBC 使用者 -> 報錯。
<?php
class Db {
private static ?self $instance = null;
private \mysqli $mysqli;
private function __construct() {
$this->mysqli = new mysqli('localhost', 'root', 'pass', 'db');
if ($this->mysqli->connect_errno) {
throw new \RuntimeException('DB 連線失敗: '.$this->mysqli->connect_error);
}
$this->mysqli->set_charset('utf8mb4');
}
public static function getInstance(): self {
return self::$instance ??= new self();
}
public function getConnection(): \mysqli { return $this->mysqli; }
public function query(string $sql): \mysqli_result|bool {
if ($this->mysqli->ping() === false) $this->reconnect();
return $this->mysqli->query($sql);
}
private function reconnect(): void {
$this->mysqli->close();
$this->mysqli = new mysqli('localhost', 'root', 'pass', 'db');
}
// 禁止複製
private function __clone() {}
public function __wakeup() {}
}
// 使用
$mysqli = Db::getInstance()->getConnection();
$result = $mysqli->query("SELECT * FROM user");
?>
<?php
class PdoDb {
private static ?\PDO $pdo = null;
public static function getPdo(): \PDO {
if (self::$pdo === null) {
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
self::$pdo = new \PDO($dsn, 'root', 'pass', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false, // 真實預處理
\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
]);
}
return self::$pdo;
}
public static function query(string $sql, array $params = []): \PDOStatement {
$stmt = self::getPdo()->prepare($sql);
$stmt->execute($params);
return $stmt;
}
}
// 使用
$users = PdoDb::query("SELECT * FROM users WHERE status = ? AND created_at > ?", [1, '2024-01-01'])->fetchAll();
$stmt = PdoDb::query("INSERT INTO logs (msg, level) VALUES (?, ?)", ['登入成功', 'info']);
?>
<?php
// PDO 持久連線 (跨請求復用,減少握手)
$pdo = new \PDO($dsn, $user, $pass, [
\PDO::ATTR_PERSISTENT => true,
// ... 其他屬性
]);
// 注意:持久連線可能導致 "Too many connections",需配合 MySQL max_connections、PHP-FPM pm.max_children 規劃
?>
生產建議:使用 ProxySQL 或 PgBouncer (PostgreSQL) 作為連線池中介,而非依賴 PHP 層持久連線。
| 錯誤訊息 | 原因 | 解決 |
|---|---|---|
| Access denied for user 'ODBC'@'localhost' | 無有效連線資源,回退 ODBC | 檢查連線變數是否被覆蓋/關閉,改用單例/PDO |
| MySQL server has gone away | 連線逾時 (wait_timeout) 或封包過大 | 查詢前 `ping()` 重連、調大 `max_allowed_packet` |
| Packets out of order | 多執行緒共用同一連線 | 每執行緒獨立連線,或用連線池 |
| Commands out of sync | 多重查詢未釋放結果集 | `$stmt->free_result()` 或 `fetchAll()` 後再查詢 |