苍穹外卖项目数据库连接问题排查与原理分析报告
一、问题背景
在启动苍穹外卖后端初始工程时,项目在初始化数据库连接阶段失败,
Spring Boot 启动报错,Druid 数据源无法正常连接 MySQL。最开始直觉上以为是数据库用户名或密码错误,但经过逐步排查后发现,问题并不是单一因素造成的,而是经历了以下几个阶段:
- 账号认证失败
- 同一 MySQL 服务下,不同客户端命中了不同的 MySQL 账户
- 项目实际命中的账户缺少目标数据库权限
- 修改配置后又引入了 YAML 语法错误
最终通过逐步定位,问题得到解决。
二、初始报错现象
项目启动时报错如下:
create connection SQLException
errorCode 1045, state 28000
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)
这类错误表面上看通常表示:
- 用户名或密码错误
- MySQL 不允许当前用户从当前主机登录
因此,最开始最自然的判断是:root 账号密码写错了 。
但是确认自己在 MySQL 客户端中输入同样的密码可以正常登录,因此说明问题不能简单归结为"密码错了",需要继续分析。
三、第一阶段分析:1045 报错的含义
1. 报错含义
1045 的核心含义是:认证失败。
也就是说:
- JDBC 已经成功访问到了 MySQL 服务
- 但 MySQL 不认可当前这组登录身份
这说明,此时不是以下问题:
- MySQL 服务未启动
- 3306 端口不通
- Druid 连接池本身故障
而是认证阶段出了问题。
2. 初步怀疑方向
结合现象,初步排查方向包括:
- 项目实际读取到的密码不是用户以为的那个密码
- Spring Boot 读取了其他配置文件,如
application-dev.yml localhost和127.0.0.1在 MySQL 中可能命中的是不同账户- Workbench、命令行、JDBC 可能使用了不同的连接方式,最终导致不同的权限结果
四、第一轮调试:验证 root 在不同 host 下的登录情况
为了验证是否是 localhost 和 127.0.0.1 的差异导致,先在命令行中测试 root 用户对不同 host 的登录情况。
1. 使用 localhost 登录
mysql -hlocalhost -P3306 -uroot -p
结果:登录成功
2. 使用 127.0.0.1 登录
mysql -h127.0.0.1 -P3306 -uroot -p
结果:
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
3. 分析
这一步非常关键,说明:
- 同样是 root
- 同样是本机
- 同样是 3306 端口
- 但是
localhost可以登录 127.0.0.1却不能登录
这表明:
MySQL 对不同 host 形式的本地连接,可能命中的是不同账户规则。
因此,不能简单地把"root 能登录"理解成"项目一定能登录"。
五、第二轮调试:查询 MySQL 中 root 账户的 host 情况
成功登录后,执行如下 SQL:
select user, host, plugin, account_locked, password_expired
from mysql.user
where user = 'root';
查询结果:
+------+-----------+-----------------------+----------------+------------------+
| user | host | plugin | account_locked | password_expired |
+------+-----------+-----------------------+----------------+------------------+
| root | % | mysql_native_password | N | N |
| root | 127.0.0.1 | mysql_native_password | N | N |
| root | localhost | mysql_native_password | N | N |
+------+-----------+-----------------------+----------------+------------------+
分析
这说明系统中并不是只有一个 root,而是至少存在三条不同的 root 账户规则:
root@%root@127.0.0.1root@localhost
虽然用户名都叫 root,但在 MySQL 的权限系统中,它们是三个不同的账户实体,权限可以彼此独立。
六、建立正确理解:三者的连接关系到底是什么
在这次排查里,最容易让人困惑的点是:
为什么 Workbench、命令行、Spring Boot 项目都在连本地 MySQL,但结果却不一样?
关键在于:它们都连到了同一个 MySQL 服务实例,但 MySQL 最终给它们匹配到的账户身份不同。
1. 三者之间的关系图
+--------------------------------------+
| MySQL Server |
| 主机: 本机 端口: 3306 |
| |
| 内部存在多条账户记录: |
| - root@localhost |
| - root@127.0.0.1 |
| - root@% |
+--------------------------------------+
▲ ▲
| |
| |
+------------------+ +----------------------+
| MySQL Workbench | | Spring Boot 项目 |
| | | Druid + JDBC |
| 一个客户端 | | 另一个客户端 |
+------------------+ +----------------------+
2. 为什么"同样是 root",结果却不一样
因为 MySQL 账户不是只看用户名,而是看:
用户名 + host
也就是说:
root@localhostroot@127.0.0.1root@%
虽然名字都叫 root,但它们是不同账户,权限也可以完全不同。
七、user() 与 current_user() 的区别
这是整个问题中最关键、最容易被忽视的一点。
在IDEA中执行为了确认 MySQL 最终到底按哪个账户做权限判断,执行:
select user(), current_user();
结果:
+----------------+----------------+
| user() | current_user() |
+----------------+----------------+
| root@localhost | root@127.0.0.1 |
+----------------+----------------+
1. user() 的含义
user() 表示:
客户端在登录时,自报家门说自己是谁。
也就是"我输入的用户名是什么,我从哪来"。
2. current_user() 的含义
current_user() 表示:
MySQL 服务器最终决定,把这次连接视为哪条账户记录。
真正用于权限判断的,不是 user(),而是 current_user()。
3. 本次问题中的意义
虽然登录时看起来像是:
root@localhost
但 MySQL 最终做权限判断时,使用的是:
root@127.0.0.1
因此,项目真正生效的权限不是 root@localhost 的权限,而是 root@127.0.0.1****的权限。
4. 用图说明 user() 和 current_user()
Spring Boot 项目
│
│ 登录时自报身份
▼
user() = root@localhost
│
│ MySQL 根据 host 规则匹配账户
▼
current_user() = root@127.0.0.1
│
│ 最终按这条账户记录判断权限
▼
是否允许访问 sky_take_out
所以,真正决定权限的关键不是"我以为我是谁",而是:
MySQL 最终把我算成谁。
八、为什么 Workbench 和项目会命中不同账户
后来在 MySQL Workbench 中执行:
select @@hostname, @@port;
select user(), current_user();
show databases like 'sky_take_out';
show grants for current_user();
Workbench 中的结果显示:
- 主机是同一台机器
- 端口是同一个 3306
user() = root@localhostcurrent_user() = root@localhost- 能看到
sky_take_out - 权限明显大于
root@127.0.0.1
分析
这说明:
- Workbench 和项目连接的是同一个 MySQL 服务实例
- 但命中的不是同一个 MySQL 账户
- Workbench 命中的是
root@localhost - 项目/JDBC 命中的是
root@127.0.0.1
因此就出现了现象差异:
- Workbench 能看到
sky_take_out - 项目不能访问
sky_take_out
更形象的说明图
同一台 MySQL Server:3306
+--------------------------------------------------+
| 账户A: root@localhost |
| └─ 权限较大,可访问 sky_take_out |
| |
| 账户B: root@127.0.0.1 |
| └─ 最初只有 malware.* 权限 |
| |
| 账户C: root@% |
+--------------------------------------------------+
▲ ▲
| |
| |
+---------------------+ +------------------------+
| Workbench | | Spring Boot + JDBC |
| current_user() | | current_user() |
| = root@localhost | | = root@127.0.0.1 |
+---------------------+ +------------------------+
| |
| |
能看到 sky_take_out 最初不能访问 sky_take_out
九、第三阶段:报错从 1045 变成 1044
后来项目报错发生变化:
errorCode 1044, state 42000
java.sql.SQLSyntaxErrorException: Access denied for user 'root'@'127.0.0.1' to database 'sky_take_out'
分析
这个变化非常重要,因为它意味着问题已经进入下一阶段。
1045:认证失败,连不上1044:已经登录成功,但没有访问某个数据库的权限
也就是说,此时问题已经从"账号能不能登录"转变成了:
账号虽然已经登录成功,但它没有 sky_take_out****这个数据库的访问权限。
十、第五轮调试:查看当前账户的授权情况
执行:
show grants for current_user();
show grants for 'root'@'127.0.0.1';
结果:
GRANT USAGE ON *.* TO `root`@`127.0.0.1`
GRANT ALL PRIVILEGES ON `malware`.* TO `root`@`127.0.0.1`
分析
这说明 root@127.0.0.1 只有两类权限:
USAGE ON *.*
-
- 基本等于"允许登录,但没有实质性全局权限"
ALL PRIVILEGES ON malware.*
-
- 只对
malware数据库有全部权限
- 只对
而对于 sky_take_out:
- 没有授权
- 因此访问时会报 1044
这正好解释了为什么项目报:
Access denied for user 'root'@'127.0.0.1' to database 'sky_take_out'
十一、关于 localhost 与 127.0.0.1 的进一步理解
通过这次排查,还需要建立一个更准确的理解:
1. localhost 和 127.0.0.1 不是简单完全等价
虽然从直觉上看,它们都指向本机,但在数据库连接场景中,它们不一定被完全一样地处理。
127.0.0.1更明确表示通过 TCP/IP 回环地址连接本机localhost在不同客户端、不同实现中,可能触发更偏"本地"的连接处理方式
因此:
- Workbench 更可能命中
root@localhost - JDBC 作为数据库驱动,更稳定地按 TCP/IP 路径工作,更容易最终命中
root@127.0.0.1
2. 这次问题中应该如何准确表述
更准确的结论不是:
JDBC 自动把 localhost 改成了 127.0.0.1
而是:
JDBC 这条连接最终通过 TCP 路径接入 MySQL,MySQL 最终将这次连接匹配为了 root@127.0.0.1 账户。
所以,最终权限判断依据的是:
current_user() = root@127.0.0.1
十二、问题根因
最终可以明确,根因不是单一问题,而是多个因素叠加:
1. MySQL 中存在多个 root 账户
分别是:
root@localhostroot@127.0.0.1root@%
这些账户虽然名字都叫 root,但权限彼此独立。
2. Workbench 和 JDBC 命中的账户不同
- Workbench:
root@localhost - 项目/JDBC:
root@127.0.0.1
3. root@127.0.0.1 最初没有 sky_take_out 库权限
它只有 malware.* 的权限,因此项目启动时无法访问 sky_take_out。
4. 后续修改配置时,又引入了 YAML 语法错误
例如把 URL 写成了:
url: { jdbc:mysql://127.0.0.1:3306/sky_take_out?... }
导致 Spring Boot 在读取配置文件时直接报错:
org.yaml.snakeyaml.parser.ParserException
while parsing a flow mapping
expected ',' or '}', but got '?'
这说明程序甚至还没开始连数据库,配置文件解析阶段就失败了。
十三、解决过程
1. 在有管理权限的 Workbench 会话中授权
由于 Workbench 命中的是 root@localhost,并且该账户拥有足够权限,因此可以用它给 root@127.0.0.1 授权:
GRANT ALL PRIVILEGES ON sky_take_out.* TO 'root'@'127.0.0.1';
再次查看授权:
SHOW GRANTS FOR 'root'@'127.0.0.1';
结果中出现:
GRANT ALL PRIVILEGES ON `sky_take_out`.* TO `root`@`127.0.0.1`
说明授权成功。
2. 修正 application.yml 配置
将 JDBC URL 改为明确使用 127.0.0.1,避免 localhost 再引发账户命中混乱。
正确写法如下:
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: "jdbc:mysql://127.0.0.1:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true"
username: root
password: "你的密码"
3. 修正 YAML 语法错误
不能写成:
url: { jdbc:mysql://127.0.0.1:3306/sky_take_out?... }
因为 {} 在 YAML 中会被解析成对象映射,而不是普通字符串。
必须写成普通字符串,最好加引号。
十四、关键报错与含义总结
1. 1045
Access denied for user 'root'@'localhost' (using password: YES)
含义:
- 用户认证失败
- 用户名/密码/host 匹配不通过
2. 1044
Access denied for user 'root'@'127.0.0.1' to database 'sky_take_out'
含义:
- 登录成功
- 但没有数据库
sky_take_out的访问权限
3. YAML ParserException
while parsing a flow mapping
expected ',' or '}', but got '?'
含义:
application.yml文件语法错误- Spring Boot 还没开始连数据库,配置解析阶段就失败了
十五、本次排查中使用的关键命令汇总
1. 测试 localhost 登录
mysql -hlocalhost -P3306 -uroot -p
2. 测试 127.0.0.1 登录
mysql -h127.0.0.1 -P3306 -uroot -p
3. 查看 root 账户列表
select user, host, plugin, account_locked, password_expired
from mysql.user
where user = 'root';
4. 查看当前真实权限身份
select user(), current_user();
5. 查看当前账户授权
show grants for current_user();
show grants for 'root'@'127.0.0.1';
6. 查看库是否可见
show databases like 'sky_take_out';
7. 给 root@127.0.0.1 授权
GRANT ALL PRIVILEGES ON sky_take_out.* TO 'root'@'127.0.0.1';
8. 验证库可用性
use sky_take_out;
show tables;
十六、经验总结
这次问题的最大收获,不只是"把项目跑起来",而是对 MySQL 权限体系、本地连接行为,以及项目配置排错逻辑有了更深入的理解。
1. MySQL 权限判断看的是"用户名 + host"
不能只看用户名。
root@localhost 和 root@127.0.0.1 虽然都叫 root,但权限可以完全不同。
2. user() 和 current_user() 很关键
user()表示登录时客户端自报的身份current_user()表示 MySQL 最终实际按哪个账户做权限判断
排查权限问题时,必须看 current_user()。
3. "能登录"不等于"有库权限"
即使某账号能进入 MySQL,也不代表它有目标数据库的权限。
认证通过只是第一步,库权限还要单独检查。
4. Workbench、命令行、JDBC 可能命中不同账户
虽然都在访问本地 MySQL,但最终匹配到的账户不一定一样,不能简单互相替代。
5. 配置文件错误会掩盖真正问题
数据库权限刚修好后,又因为 YAML 写法错误导致程序启动失败。
所以调试时要区分:
- 配置解析错误
- 数据库连接错误
- 权限错误
这三类问题不在同一个层面。
6. 连接地址最好统一
在本地开发中,如果已经确认项目实际按 TCP 路径访问数据库,那么 JDBC URL 直接写成 127.0.0.1 更清晰,也更利于排查。
十七、最终结论
本次苍穹外卖项目数据库连接问题,根本原因是:
项目通过 JDBC 连接 MySQL 时,最终命中了 root@127.0.0.1****账户,而该账户最初没有 sky_take_out****数据库的访问权限;同时在修改配置过程中,又引入了 YAML 语法错误,导致 Spring Boot 启动失败。
最终通过以下措施解决:
- 明确区分
root@localhost与root@127.0.0.1 - 使用具有管理员权限的
root@localhost给root@127.0.0.1授予sky_take_out.*权限 - 将 JDBC 连接地址统一为
127.0.0.1 - 修正
application.yml中 JDBC URL 的 YAML 语法错误
至此,项目数据库连接问题得到解决。
十八、一句话总结这次问题的本质
不是 Workbench 和项目连了不同的数据库,而是它们连了同一个 MySQL 服务,但 MySQL 最终把它们识别成了两个不同的账户身份。