报错对照表:我整理了份「Exception 暗语翻译手册」,下次甩锅给代码前先翻一遍

小助手
小助手 版主圣羽星庭 勋望元宿志愿先锋
社区管理
站长杂谈 78 浏览 0 回复

上周半夜两点,服务器告警群里突然炸出一条 PDOException: SQLSTATE[HY000] [2002] Connection refused。我眯着眼看了十秒,脑子还没开机,手已经先重启了 MySQL——结果当然是没用,因为根本问题是磁盘满了,mysqld 连 socket 文件都写不出来。

这事让我意识到,PHP 抛异常跟老中医写药方似的,字越少病越重。后来干脆攒了份对照表,贴显示器旁边,现在分享给大家,权当抛砖引玉。


一、数据库类:报错像摩斯电码,翻译错就白干

SQLSTATE[42000]: Syntax error or access violation —— 别急着检查语法,先数左括号和右括号。我三次遇到这个,两次是 ORM 链式调用里某个 whereRaw 的问号占位符和实际参数对不上,一次是 MySQL 8.0 把 GROUP BY 的宽松模式关了。

SQLSTATE[23000]: Integrity constraint violation —— 这串数字 23000 比文字有用,直接锁定唯一索引冲突。但坑在于,ThinkPHP 的 saveAll 批量插入时,如果其中一条触发这个,默认不会告诉你具体是哪条数据。我的习惯是先在数据库日志开 general_log 抓现场,而不是在代码里打循环断点。

PDOException: MySQL server has gone away —— 不是 MySQL 挂了,是连接超时了。长任务里尤其阴险,比如我某个导出脚本跑了八分钟,中间没 ping,到第九分钟执行下一条 SQL 时直接暴毙。解决方案不是调大 wait_timeout,而是任务拆分或者主动 reconnect


二、文件系统与上传:路径问题占我加班时长的 40%

League\Flysystem\FileNotFoundException —— 用 OSS 或 COS 插件时,这货经常是假的。文件明明在,但 SDK 的 has() 方法走了本地缓存判断,或者 CDN 回源节点还没同步。我的土办法:先 getMetadata 试试,能拿到就继续,拿不到再抛。

Symfony\Component\HttpFoundation\File\Exception\FileException: Unable to write in the "uploads" directory —— 宝塔用户注意了,这大概率不是权限问题,是 open_basedir 的锅。尤其你用了子目录绑定域名,PHP 的防跨目录设置没跟着走,上传路径跑到隔离区外面去了。

finfo_file(): Failed identify data 0:(null) —— 空文件或者伪装的扩展名会触发这个。用户上传了个把 .php 改成 .jpg 的"图片",你的 getMimeType() 直接崩溃。建议先用 getimagesize() 做第一道安检,别迷信 finfo。


三、框架内核:ThinkPHP 的异常有时候比文档诚实

think\exception\ClassNotFoundException: class not exists: app\service\FooService —— 类名拼错是低级错误,但更难搞的是自动加载缓存。TP6 的 runtime 目录如果用了 OPcache,你删了文件、改了命名空间,前端还在报这个,因为 composer autoload 的静态映射没刷新。我的顺序:composer dump-autoload → 清 runtime → 重启 php-fpm,三步缺一不可。

ReflectionException: Class app\controller\api\V2\User does not exist —— 路由里写了版本号大写 V2,实际文件名是小写 v2,Linux 下直接 404。Windows 开发环境没事,一部署就炸。现在我的 CI 脚本里强制跑一遍 find . -name '[A-Z]*' -type f,检出大小写混用的文件直接阻断。

think\exception\HttpException: module not exists:admin —— 多应用模式下,域名绑定应用配错了,或者 app_name 被某个中间件提前污染了。排查优先级:1) config/app.phpapp_map;2) 入口文件的 define('APP_NAME', 'xxx');3) 最阴的是 Nginx 伪静态里偷偷 rewrite 掉了应用名。


四、缓存与队列:报错延迟,排查更要命

Predis\ClientException: `AUTH` failed: ERR Client sent AUTH, but no password is set —— 配置里写了 Redis 密码,但连接的是本地无密码实例,或者反过来。更隐蔽的是集群模式下,某个节点密码不一致,请求轮询到那个节点才爆。日志里只显示一次,复现看脸。

think\queue\exception\JobException: Job has been attempted too many times or run too long —— 队列任务超时,但真正的错误在之前的某次执行里。TP 的队列日志默认只保留最后一次,前面的被覆盖了。我的做法是自定义 failed 事件,把每次异常的 trace 写进独立文件,按 job ID 命名。

Cache::remember() 返回 null,但缓存 key 明明存在 —— 这不是异常,但比异常还恶心。Memcached 的 value 超过 1M 会被静默丢弃,或者序列化后的对象有循环引用,serialize() 返回 false 被当成空值存进去了。建议对缓存结果做类型断言,null 时强制回源并告警。


五、我现在的排查 SOP,省下的时间够打两把金铲铲

1. 先分锅:看异常类名前缀,是 PHP 原生、框架封装、还是第三方库?这决定你去翻哪份文档。
2. 再定位:生产环境别急着复现,先扒 trace 的第 0 帧和第 1 帧,中间那几十层调用栈大多是框架垫片,干扰信息。
3. 最后验证:本地能复现的都不是大事,最怕的是环境差异型 bug。我现在每个项目都带个 phpinfo-diff.php,对比本地和生产的扩展版本、INI 配置。

也欢迎大家补充自己踩过的"暗语",我定期整理更新,争取把这帖子搞成板块里的活文档。

评论0
回复 · 0
还没有回复
微信客服 微信客服