
前言连接本地 MySQL 报错是一类特别容易误判的问题因为它给出的提示常常指向错误的方向。最典型的是这样报错说Access denied于是去反复确认密码其实真正原因是这个账号只授权给了某个特定的 host或者报错说找不到套接字文件于是去重装 MySQL其实服务好好地跑着只是 PHP 编译时用的套接字路径和实际路径不是同一个。要跳出这种打转有两个习惯必须养成。第一先看错误号再动手。MySQL 客户端库返回的数值错误码1045、2002、1049 之类几乎直接指明了故障层面比错误文字的措辞可靠得多。第二分清连不上服务和连上了但被拒绝。前者是网络或进程的问题后者是认证与授权的问题两者要查的东西完全不同。本文按先拿到错误号 → 排查连不上 → 排查认证失败 → 给出正确的连接写法的顺序讲。文中涉及的具体错误码与提示文字以你的 MySQL/MariaDB 版本和 PHP 版本的实际输出为准示例代码是静态推演的未在真实环境执行验证。一、先拿到真实的错误信息不要靠页面上那句数据库连接失败猜原因先把驱动给的原话和错误号取出来。用mysqli的过程式写法?php// 适用于 PHP 5.3注意 PHP 8.1 起 mysqli 默认会抛异常见下$link mysqli_connect(127.0.0.1, app, secret, demo, 3306);if ($link false) {printf(错误号: %d\n, mysqli_connect_errno());printf(错误信息: %s\n, mysqli_connect_error());exit;}printf(连接成功服务端版本: %s\n, mysqli_get_server_info($link));这里必须提一个版本差异从PHP 8.1 起mysqli的默认错误模式从静默返回 false改成了抛出mysqli_sql_exception。也就是说上面这段在 PHP 8.1 上如果连接失败会在mysqli_connect()那一行直接抛异常$link false这个判断根本走不到。要兼容两种行为用try/catch包起来?php// 适用于 PHP 8.1 的写法同时兼容更低版本mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);try {$link new mysqli(127.0.0.1, app, secret, demo, 3306);$link-set_charset(utf8mb4);echo 连接成功;} catch (mysqli_sql_exception $e) {// 记日志不要把详细信息直接输出给访客error_log(sprintf(MySQL 连接失败 [%d] %s, $e-getCode(), $e-getMessage()));http_response_code(500);exit(服务暂不可用);}用PDO时异常对象上带着结构化的错误信息?php// 适用于 PHP 7.0try {$pdo new PDO(mysql:host127.0.0.1;port3306;dbnamedemo;charsetutf8mb4,app,secret,[PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION]);} catch (PDOException $e) {// errorInfo 的第 0 项是 SQLSTATE第 1 项是驱动错误号$driverCode $e-errorInfo[1] ?? 未知;error_log(PDO 连接失败驱动错误号 {$driverCode} . $e-getMessage());exit(服务暂不可用);}拿到错误号之后对照下面这张表定位错误号提示大意故障层面2002Cant connect to local MySQL server through socket走 Unix 套接字连接失败2003Cant connect to MySQL server on 127.0.0.1走 TCP 连接失败1045Access denied for user xlocalhost认证失败密码、账号或 host 不匹配1049Unknown database x库不存在1044Access denied for user ... to database账号没有该库的权限1040Too many connections连接数已达上限1130Host x is not allowed to connect该账号的授权 host 不包含请求来源二、2002 与 2003连不上服务这两个错误码的区别本身就是最有用的线索2002表示 PHP 尝试通过Unix 套接字文件连接但那个文件不存在或连不上。2003表示 PHP 尝试通过TCP连接但目标地址端口没有响应。为什么同样是写localhost结果却不一样因为在 MySQL 客户端库里localhost是一个特殊值它表示走 Unix 套接字而127.0.0.1才表示走 TCP 回环地址。命令行的mysql客户端也遵循这个规则但它会去读/etc/my.cnf之类的配置拿到实际套接字路径而 PHP 用的是一个编译期或php.ini里指定的默认路径。两边不一致时就会出现命令行mysql -u root能连上PHP 连同一个库却报 2002的诡异现象。处理方法按优先级排列改用127.0.0.1强制走 TCP。这是最省事也最不容易出错的方案?php// 适用于 PHP 7.0$pdo new PDO(mysql:host127.0.0.1;port3306;dbnamedemo;charsetutf8mb4, $user, $pass);如果确实要用套接字显式指定路径别依赖默认值。PDO 用unix_socket?php// 适用于 PHP 7.0$pdo new PDO(mysql:unix_socket/run/mysqld/mysqld.sock;dbnamedemo;charsetutf8mb4,$user,$pass);mysqli则把套接字作为第六个参数传第五个是端口?php// 适用于 PHP 5.3// mysqli_connect(主机, 用户名, 密码, 库名, 端口, 套接字)$link new mysqli(localhost, app, secret, demo, 3306, /run/mysqld/mysqld.sock);或者统一在php.ini里设定默认套接字让两边对齐; 让 mysqli 与 PDO 默认使用与实际一致的套接字路径mysqli.default_socket /run/mysqld/mysqld.sockpdo_mysql.default_socket /run/mysqld/mysqld.sock排查服务本身是否在跑用系统命令确认不要靠猜# 看服务状态systemd 发行版systemctl status mysql# 或systemctl status mariadb# 看端口有没有在监听ss -lntp | grep 3306# 用客户端自带工具探测mysqladmin ping -h 127.0.0.1 -u app -p如果ss显示 3306 只有127.0.0.1在监听那么从别的机器连不上是预期行为不是故障——这是bind-address在起作用。反之如果配置里写了skip-networking则 TCP 完全关闭只能走套接字。这两项都属于服务端故意这么配的改之前要想清楚安全含义。三、1045 与 1130连上了但被拒绝1045 是最容易被误判的一类。它的提示大概是Access denied for user applocalhost (using password: YES)几个细节都藏着信息using password: YES还是NO。如果写的是NO而你明明传了密码说明密码根本没传过去——常见于从环境变量读取时变量名为空、配置文件里多了一层引号、或者变量名拼错。这种情况下错误信息其实在告诉你我没收到密码。applocalhost里的 host 部分是关键。MySQL 的账号是用户名 来源 host的组合applocalhost和app127.0.0.1是两个不同的账号各自的密码和权限独立。只建了前者用 TCP127.0.0.1连过去就会匹配不上报 1045反过来也一样。using password: YES且密码确认无误那就要怀疑认证插件。MySQL 8.0 默认的认证插件是caching_sha2_password而它需要 mysqlndPHP 7.4 及以上才支持。用更早的 PHP 连 MySQL 8会看到类似The server requested authentication method unknown to the client的提示。正确的解法是升级 PHP为兼容老 PHP 而把账号改成mysql_native_password属于安全性降级而且较新的 MySQL 版本已经默认禁用该插件只能当作临时手段。1130 则是明说你这个来源地址不在授权范围内处理方式是给账号补上对应的 host或建一个 host 匹配的账号。排查账号本身可以登录后查-- 看有哪些账号、各自用什么认证插件、授权给哪些 hostSELECT user, host, plugin FROM mysql.user ORDER BY user, host;-- 看某个账号被授予了哪些权限SHOW GRANTS FOR app127.0.0.1;还有一个在 Ubuntu、Debian 系的 MySQL 与 MariaDB 上极常见的坑rootlocalhost默认用auth_socket/unix_socket插件只有以操作系统 root 身份通过套接字登录才能免密进入PHP 拿密码去连必然 1045。这类环境里正确的做法不是去改 root 的认证方式而是为应用单独建一个带密码的最小权限账号-- 建库并用 utf8mb4支持 4 字节字符如 emojiCREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 建一个只能从本机 TCP 连的应用账号CREATE USER app127.0.0.1 IDENTIFIED BY 换成强密码;-- 只给业务需要的权限不要 GRANT ALL更不要用 rootGRANT SELECT, INSERT, UPDATE, DELETE ON demo.* TO app127.0.0.1;四、自查脚本与安全注意把排查逻辑固化成一个小脚本比每次靠记忆快得多?php// 适用于 PHP 7.0fsockopen、PDO、空合并运算符在 PHP 7 上均可用declare(strict_types1);$host 127.0.0.1;$port 3306;$user getenv(DB_USER) ?: ;$pass getenv(DB_PASS) ?: ;$name demo;// 1. 端口通不通先于 MySQL 协议判断$sock fsockopen($host, $port, $errno, $errstr, 3);if ($sock false) {printf(端口未响应%s (%d)检查服务是否启动、bind-address 与防火墙\n, $errstr, $errno);exit(1);}fclose($sock);echo 端口 {$port} 可连接\n;// 2. 再做一次真正的连接try {$pdo new PDO(mysql:host{$host};port{$port};dbname{$name};charsetutf8mb4,$user,$pass,[PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION,PDO::ATTR_TIMEOUT 3, // MySQL 驱动下这是连接超时秒]);printf(连接成功服务端 %s\n, $pdo-query(SELECT VERSION())-fetchColumn());} catch (PDOException $e) {printf(连接失败 [%s] %s\n, $e-errorInfo[1] ?? ?, $e-getMessage());exit(1);}安全上有三条底线不要把这些诊断信息直接输出给网页访客。错误原文里可能带主机名、用户名、库名。用error_log()记到服务端日志页面上只回一句笼统的提示。凭据不要写在 Web 根目录下的 PHP 文件里放环境变量或 Web 目录之外、权限收紧的配置文件。应用账号遵循最小权限不要图省事给ALL PRIVILEGES更不要用root跑业务代码。常见坑点❌mysqli_connect()失败后去看$link-connect_error结果报在 null 上调用属性✅ 连接失败时返回的不是对象必须用函数式的mysqli_connect_errno()与mysqli_connect_error()PHP 8.1 起默认直接抛mysqli_sql_exception要用try/catch。❌ 命令行mysql -h localhost能连PHP 写localhost却报 2002✅localhost在客户端库里表示走 Unix 套接字而 PHP 用的默认套接字路径可能与命令行的不一致。改成127.0.0.1走 TCP或在 DSN /php.ini里显式指定套接字路径。❌ 报 1045 就反复改密码改了还是不行✅ 先看提示里的 host 部分。applocalhost与app127.0.0.1是两个不同账号再看using password是 YES 还是 NO如果传了密码却显示 NO说明密码没传进去。❌ 直接拿root账号连库在 Ubuntu/Debian 上永远 1045✅ 这类系统的rootlocalhost默认使用auth_socket插件只认同机 root 身份的套接字登录。为应用单独建带密码的最小权限账号。❌ 从.env或配置文件读密码时没去除首尾空白✅ 文件末尾的换行、KEY value形式里的空格都会被当成密码的一部分结果就是稳定复现的 1045。取值后trim()并把密码用引号明确包起来。❌ 用 PHP 5.x 或 7.0 连 MySQL 8报认证方法不被支持✅ MySQL 8 默认的caching_sha2_password需要较新的 mysqlnd。首选升级 PHP把账号降级成mysql_native_password只是临时兼容手段且新版 MySQL 已默认禁用该插件。❌ 页面报错里直接显示连接异常原文暴露了主机名、用户名、库名✅ 用error_log()记录详情页面上只回笼统提示。生产环境还必须确保display_errors处于关闭状态。❌ 建账号时图方便用CREATE USER app%并授予全部权限✅%表示任意来源主机等于把数据库暴露给整个网络。本机应用就限制为127.0.0.1并只授予业务真正需要的那几个权限。总结错误号层面首查项2002套接字连接失败套接字路径是否一致或改用127.0.0.12003TCP 连接失败服务是否启动、端口是否监听、bind-address与防火墙1045认证失败账号的 host 部分、密码是否真的传进去、认证插件1049库不存在dbname拼写、是否已建库1044无库权限SHOW GRANTS检查授权范围1130来源未授权账号的 host 授权范围1040连接数打满连接是否泄漏、max_connections设置解决连接错误的关键不是记住每种报错的解法而是按服务在不在 → 通不通 → 认不认 → 有没有权限的顺序逐层验证。绝大多数本地连不上的问题落到最后都是两件事localhost与127.0.0.1走的通道不同以及账号的 host 与认证插件没对上。