日期:2026-10-11 | 标签:MySQL · macOS · launchd · 排错
本地 MySQL 的 root 密码忘了。
听起来是个五分钟的活:--skip-grant-tables 起一下,改掉密码,收工。结果前前后后折腾了好几轮,顺手翻出六个坑。有些是环境本身的坑,有些是我自己的误判。都记下来。
环境:macOS,MySQL 8.0.18(dmg 安装包),监听 3306,由 launchd 托管。
第一个弯路:以为项目配置里会有密码
直觉是去自己项目里翻——Spring Boot 的 application-dev.properties、Go 的 settings.yml、docker-compose,这些地方经常躺着明文密码。
确实翻到了:
# Spring Boot 项目
spring.datasource.url=jdbc:mysql://localhost:3307/low_code
spring.datasource.username=user
spring.datasource.password=password
# Go 项目
database:
username: root
password: 123456
host: 127.0.0.1
port: 13306
但一个都试不通。因为这些凭据根本不是 3306 那个实例的——它们指向的是项目自带的 Docker MySQL(一个在 3307,一个在 13306)。我盯着一个不存在的关联试了半天。
教训一:一个项目里能翻到的数据库密码,只对写下它的那个环境有效。端口不一样,就是另一台机器。
坑一:机器上不止一个 MySQL
试密码失败后,先把环境摸了一遍,才发现这台机器上装了三套 MySQL:
| 实例 | 位置 | 端口 | 托管方式 |
|---|---|---|---|
| MySQL 8.0.18(dmg 安装包) | /usr/local/mysql |
3306 | 系统级 LaunchDaemon |
| MySQL 9.7.1(Homebrew) | /opt/homebrew/var/mysql |
3306(抢不到) | 用户级 LaunchAgent |
| Docker MySQL ×2 | 项目自带 | 3307 / 13306 | docker compose |
两个本地实例抢同一个 3306。
dmg 那个是 2019 年的安装包,注册的是系统级守护进程(/Library/LaunchDaemons/),开机自动启动、以 _mysql 用户运行。
Homebrew 那个用的是用户级 LaunchAgent(~/Library/LaunchAgents/),默认不会开机自启——但它有个致命的配置:
<key>KeepAlive</key>
<true/>
这个 KeepAlive 意味着:只要进程退出,launchd 就立刻把它重新拉起来。
于是形成了一个死循环:Homebrew 版每次启动都因为 3306 被占而失败退出 → launchd 立刻重启 → 又失败……它的错误日志里累计了 5800 多次重启记录,一直在后台空转烧 CPU。
[ERROR] [MY-010262] [Server] Can't start server: Bind on TCP/IP port: Address already in use
[ERROR] [MY-010257] [Server] Do you already have another mysqld server running on port: 3306 ?
[ERROR] [MY-010119] [Server] Aborting
教训二:pkill mysqld 对配了 KeepAlive 的服务是没用的——你杀掉它,launchd 会立刻把它拉起来。必须用 launchctl bootout 把服务整个卸载:
launchctl bootout gui/$UID/homebrew.mxcl.mysql
这还引出一个更隐蔽的问题:重置密码时需要临时停掉 3306 上的实例,而一旦它停下,那个 KeepAlive 的实例就会立刻抢占端口,导致你恢复服务时反而绑不上。所以必须先卸载冲突的服务,再动手。
坑二:ERROR 2059 —— 客户端比服务器新太多
密码重置后,兴冲冲敲了个 mysql,结果:
ERROR 2059 (HY000): Authentication plugin 'mysql_native_password' cannot be loaded:
dlopen(/opt/homebrew/Cellar/mysql/9.7.1/lib/plugin/mysql_native_password.so, 0x0002):
tried: '/opt/homebrew/Cellar/mysql/9.7.1/lib/plugin/mysql_native_password.so' (no such file)
注意这里根本没有走到密码验证。这是客户端在加载认证插件时失败的。
原因:服务器是 8.0.18,并且 plist 里强制了老式认证插件:
--default_authentication_plugin=mysql_native_password
而终端里的 mysql 命令,走的是 PATH 里排第一的 Homebrew 9.7.1 客户端。MySQL 9.x 已经彻底移除了 mysql_native_password 插件,所以它根本不认识服务器要求的认证方式。
同一个服务器,用配套的客户端立刻就通:
$ /usr/local/mysql/bin/mysql -e "SELECT VERSION();"
+-----------+
| VERSION() |
+-----------+
| 8.0.18 |
+-----------+
教训三:MySQL 客户端和服务器的版本要对得上。ERROR 2059 这个报错看着像密码问题,其实是插件加载失败,别在密码上浪费时间去猜。
坑三:两个把我带偏的”证据”
这一轮里有两个观察结论是错的,值得单独拎出来。
1. lsof 看不见它
排查端口占用时我跑了 lsof -iTCP -sTCP:LISTEN | grep mysql,没有任何输出。差点得出”MySQL 没在跑”的结论。
实际上它在跑得好好的。原因是它以 _mysql 用户的身份运行,而普通用户的 lsof 读不到别人进程的 fd,于是静默忽略了。正确的判断方式是看进程:
pgrep -fl mysqld
2. zsh 里根本没有 /dev/tcp
我用 (exec 3<>/dev/tcp/127.0.0.1/3306) 探测端口通不通,结果 3306、3307、13306 全部报”关闭”。这个结论完全是假的。
/dev/tcp 是 bash 的特性,zsh 不支持。那行重定向直接失败,被我 || echo "关闭" 接住了,于是每个端口都”关闭”。真实的服务器其实一直在正常应答。
教训四:探测端口用客户端实测,别用 shell 的旁门左道:
mysql -h 127.0.0.1 -P 3306 -u root -e "SELECT 1;"
# 返回 ERROR 1045 Access denied → 服务器活着,只是密码不对
# 返回 Can't connect (61) → 端口真的没人监听
正解:--skip-grant-tables 重置密码
绕了一圈,回到最标准的做法。流程是:停服务 → 以跳过权限校验的模式启动 → 改密码 → 恢复正常服务。
① 停掉 launchd 托管的实例
sudo launchctl bootout system /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist
② 以跳过权限校验的方式启动
sudo /usr/local/mysql/bin/mysqld --user=_mysql \
--basedir=/usr/local/mysql --datadir=/usr/local/mysql/data \
--skip-grant-tables --skip-networking
注意 --skip-networking:这个模式下任何人都能免密进去,关掉 TCP 让它只监听本地 socket。
③ 另一个终端里连进去改密码
/usr/local/mysql/bin/mysql -u root --socket=/tmp/mysql.sock
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的新密码';
FLUSH PRIVILEGES;
顺序不能反:必须先 FLUSH PRIVILEGES 把权限表重新加载,ALTER USER 才会生效。
④ 关掉临时实例,恢复服务
sudo launchctl bootstrap system /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist
数据不会丢。这个操作只改 mysql.user 表里的密码字段,表和数据原封不动。
坑四:手工起 mysqld 时,TMPDIR 会让 InnoDB 启动失败
把上面的流程写进脚本自动执行时,临时实例起不来了:
mysqld: Can't create/write to file '/var/folders/gg/.../T/ibYXaosr' (OS errno 13 - Permission denied)
[ERROR] [MY-012576] [InnoDB] Unable to create temporary file; errno: 13
[ERROR] [MY-012930] [InnoDB] Plugin initialization aborted
[ERROR] [MY-010119] [Server] Aborting
原因很有意思。mysqld 是以 root 启动、然后降权成 _mysql 用户运行的。而启动它的那个环境里,TMPDIR 指向用户的私有临时目录 /var/folders/gg/...——那个目录属于 zhaozhenhui,_mysql 写不进去。
launchd 启动同样的服务时为什么没事?因为系统级守护进程的环境是干净的,没有 TMPDIR,mysqld 就退回到 /tmp。
解法是复现 launchd 的干净环境:
env -i /usr/local/mysql/bin/mysqld --user=_mysql \
--datadir=/usr/local/mysql/data \
--tmpdir=/tmp \
--skip-grant-tables --skip-networking
env -i 清空所有环境变量,再加一个显式的 --tmpdir=/tmp 双保险。
教训五:手工拉起的守护进程,环境和 launchd 拉起的完全不是一回事。降权运行的进程尤其要留意 TMPDIR、HOME 这类指向用户私有目录的变量。
收尾:两件让下次省事的事
~/.my.cnf:免密登录
printf '[client]\nuser=root\npassword=你的新密码\n' > ~/.my.cnf
chmod 600 ~/.my.cnf
之后 mysql、mysqladmin、mysqldump 都不用手输密码了。权限必须是 600——这是明文密码文件。
注意:Navicat 之类的 GUI 工具不读 ~/.my.cnf,还是得手动填密码。
PATH 顺序:.zshenv 里写了却不生效
前面说 9.7.1 客户端连不上,那让它默认走 8.0.18 的客户端不就行了?于是在 ~/.zshenv 末尾加了一行:
export PATH="/usr/local/mysql/bin:$PATH"
结果 which mysql 还是指向 /opt/homebrew/bin/mysql。
查了一下 PATH 的顺序:
/Users/zhaozhenhui/.nvm/versions/node/v22.23.3/bin
/opt/homebrew/bin ← 还是它在前面
...
/usr/local/mysql/bin ← 我加的这个排到第 18 位了
zsh 的配置文件加载顺序是:
.zshenv → .zprofile → .zshrc
而 ~/.zprofile 里有这么一行:
eval "$(/opt/homebrew/bin/brew shellenv)"
brew shellenv 会把 /opt/homebrew/bin 插到 PATH 最前面。它在 .zshenv 之后执行,于是后来居上,把刚加进去的 MySQL 路径压到了后面。which 只认第一个匹配,自然还是选中了 Homebrew 的客户端。
改法是把那一行挪到 ~/.zprofile 里 brew shellenv 的后面,或者放进最后加载的 ~/.zshrc:
eval "$(/opt/homebrew/bin/brew shellenv)"
# 必须在 brew shellenv 之后:Homebrew 的 9.x 客户端缺 mysql_native_password 插件
export PATH="/usr/local/mysql/bin:$PATH"
教训六:往 shell 配置里加 PATH,光看”加进去了”没用,得看最终顺序。哪个文件后加载,它插的路径就排在前面。
复盘
回头看,真正花时间的不是重置密码本身,而是三件事:
- 猜密码。试了六七个候选,全是无用功。MySQL 默认没有失败锁定,猜着不费劲,但命中率极低——忘了密码就直接重置,别猜。
- 误判。
lsof看不见、/dev/tcp全是假阴性,这两条错误”证据”各自把我带偏了一轮。工具给出的空结果,要先问一句”是真的没有,还是我没看到”。 - 环境差异。同一个 mysqld,launchd 启动和手工启动行为不同;同一个
mysql命令,PATH 顺序不同指向完全不同的版本。这类差异不会报错,只会静默地给你错误的结果。
最省时间的做法,其实是一开始就把环境摸清楚:有几个实例、谁在哪个端口、由谁托管、客户端是哪个版本。这些搞清楚,后面每一步的判断才站得住。
本文记录的是本机环境的排查过程,路径、端口、用户请按自己机器调整。
