很多人第一次在 Windows 上被 MySQL 挡住,都不是因为 SQL 写不出来,而是被三件事卡住:root 密码彻底想不起来、要给同事或某个应用单独开个账号、开完账号发现人家连不进自己的库。这三件事单看都不难,难的是它们串在一起——先用错误的方式重置密码把服务搞挂,再手忙脚乱地建用户,最后授权授多了没法收回。这篇就把Windows 下 MySQL 忘记密码的处理、添加新用户、为用户创建数据库、给新用户分配权限这一整条链路拆开讲透,从服务名怎么确认、配置文件在哪,到 5.7 和 8.0 在密码逻辑上的分叉,再到 host 匹配规则和最小权限怎么落地,都给出可以直接照着敲的语句。适合手上正在维护一台 Windows MySQL 实例的人,也适合刚接手别人留下的数据库、需要把账号体系重新理顺的读者,哪怕你之前只会用图形化工具点鼠标,跟着走一遍也能自己完成。
1. 动手之前先把这台 MySQL 的"底细"摸清
在 Windows 上做 MySQL 的任何账号操作,失败案例里有相当一部分不是 SQL 写错,而是一开始就找错了对象:机器上装了两套 MySQL、服务名不叫 MySQL、配置文件根本不在安装目录里、命令行里的mysql命令指向的是另一个版本的客户端。所以第一步不是急着敲net stop,而是花三分钟把环境确认清楚,这三分钟能省掉后面半小时的折腾。
1.1 服务名、配置文件、bin 目录,三样必须先确认
Windows 版 MySQL 是以系统服务的形式常驻的,重置密码的第一步永远是停服务,而停服务得先知道服务名。最常见的几个名字是MySQL、MySQL57、MySQL80、MySQL84,具体取决于安装时的版本和自定义命名。查法有好几种,最干脆的是命令行:
sc query state= all | findstr /I mysql输出里SERVICE_NAME后面的那一串就是真名。另一个同样有用的命令是查服务配置:
sc qc MySQL80这条命令的输出里有一行BINARY_PATH_NAME,形如:
"D:\mysql\bin\mysqld.exe" --defaults-file="C:\ProgramData\MySQL\MySQL Server 8.0\my.ini" MySQL80这一行信息量极大:它同时告诉了你mysqld.exe的真实路径和--defaults-file指向的配置文件位置。为什么要专门查这个?因为用 MySQL Installer 装的实例,配置文件默认在C:\ProgramData\MySQL\MySQL Server 8.0\my.ini,而ProgramData是隐藏目录,很多人打开 C 盘翻半天找不到,误以为是绿色版没有配置文件。而用 ZIP 压缩包手动解压的实例,配置可能就在解压目录下,甚至压根没有my.ini,全靠默认值跑。这两种情况的后续操作路径是不一样的。
还有一个容易被忽略的点:mysql.exe客户端也要用对应版本的。如果机器上同时存在 5.7 和 8.0,PATH 里排在前面的那个客户端去连 8.0 的服务端,可能因为认证插件不兼容而报错,让你误判成密码问题。确认版本很简单,登录后执行SELECT VERSION();,没登录时用mysql --version看客户端版本。服务端版本和客户端版本分别确认一遍,这个习惯值得养成。
1.2 用管理员权限开命令行,不是形式主义
Windows 上停服务和改ProgramData下的文件都需要管理员权限。普通权限的命令行执行net stop MySQL80会直接返回"发生系统错误 5,拒绝访问",而你去改配置文件保存时会被提示需要提权。更隐蔽的坑是:用普通权限启动mysqld --console时,如果它需要往 data 目录写日志或临时文件,可能启动到一半失败,日志信息还特别含糊。
所以整个流程里,准备两个管理员权限的命令行窗口是最省事的做法。一个用来前台启动mysqld并观察输出,另一个用来执行mysql客户端命令。注意是"以管理员身份运行",不是从开始菜单普通打开,Win10/Win11 下在开始菜单搜索cmd后右键选择"以管理员身份运行"。PowerShell 也可以,但要注意 PowerShell 里执行带引号的路径参数时引号处理规则和 cmd 略有差别,新手容易在这一步把路径搞错,我一般建议涉及长路径参数时统一用 cmd。
顺带提一句权限的边界:如果你只是要建用户、建库、授权,而 root 密码你是知道的,那么完全不需要停服务,直接登录执行 SQL 就行。停服务、跳过权限表这一整套操作只服务于一个目的——在不知道 root 密码的前提下拿回控制权。把这两类操作分清楚,能避免"明明知道密码还去停服务"这种没必要的风险动作。
1.3 5.7 和 8.0 在"密码"这件事上是两套逻辑
这一条必须先说,因为它是后面所有报错的根源。MySQL 5.7 的mysql.user表里密码字段叫password,可以用UPDATE mysql.user SET password=PASSWORD('新密码') WHERE user='root';这类写法(虽然不推荐,但教程里到处都是)。到了 8.0,password字段被删除,取而代之的是authentication_string,而且PASSWORD()函数已经被移除,照抄老教程执行会直接报函数不存在的语法错误。
更关键的是认证插件的变化。5.7 默认用mysql_native_password,8.0 默认改成caching_sha2_password。这个改动带来的连锁反应是:一些老版本的图形化客户端、老版本的编程语言驱动、老版本的中间件连 8.0 时会报Authentication plugin 'caching_sha2_password' cannot be loaded。你新建的用户自己用命令行能连,业务那边就是连不上,问题就出在这里。
还有一个差异在权限语句上。8.0 之前,GRANT语句可以顺带创建用户,写GRANT ALL ON db.* TO 'u'@'%' IDENTIFIED BY 'pwd';是合法的;8.0 开始GRANT不能再隐式创建用户,必须先CREATE USER再GRANT,否则报语法错误。这两点差异,是"照着旧文档操作失败"的最常见原因。先把版本确认下来,后面的每条语句才好对号入座。
2. 忘记 root 密码:两条恢复路径与它们的适用边界
拿回 root 密码,Windows 上实际可用的路径就两条:让服务端跳过权限表启动,或者用初始化脚本在启动瞬间把密码改掉。网上还有第三种说法——用mysqld --initialize重新初始化数据目录,这条路千万别走,它的效果是清空重建数据目录,等于把现有所有库和用户全部格式化。看到把--initialize当成"重置密码"来推荐的教程,可以直接关掉。
2.1 路径一:跳过权限表启动的完整链条
这是最通用的一条路,适用于本地能物理接触机器、可以停服务的场景。步骤如下,每一步的意图我都会说清楚,因为顺序错了就会卡住。
第一步,停掉正在运行的服务:
net stop MySQL80如果提示服务没有启动,说明它本来就没跑,那更好,直接进下一步。如果提示有依赖服务无法停止,检查是不是还有别的 MySQL 实例或监控程序挂着它。
第二步,用跳过权限表的方式前台启动mysqld。这里必须带上--defaults-file,因为不带的话mysqld会去默认位置找配置,可能和你实际用的不是同一份:
"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld.exe" --defaults-file="C:\ProgramData\MySQL\MySQL Server 8.0\my.ini" --skip-grant-tables --console--skip-grant-tables的作用是启动时不加载权限表,所有连接默认拥有全部权限,也就不校验密码。--console的作用是把日志直接输出到当前窗口,你能立刻看到ready for connections或者启动失败的原因,比去翻.err日志文件快得多。这个窗口在整个过程中要保持打开,它就是数据库服务本身,关掉服务就没了。
第三步,另开一个管理员命令行窗口,不带密码直接登录:
"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root如果能进到mysql>提示符,说明这条路走通了。
2.2 8.0 上跳过权限表会顺手关掉网络监听
这是个非常容易踩的坑。在 8.0 中,--skip-grant-tables会自动启用--skip-networking,也就是服务端不再监听 TCP 端口。表现出来的现象是:你在本机用命名管道或者共享内存能连上,但用-h 127.0.0.1连不上,报ERROR 2003 (HY000): Can't connect to MySQL server on '127.0.0.1' (10061);或者你是想从另一台机器连过来改密码,那就完全没戏。很多人以为是自己密码没配对,其实服务根本没开端口。
解决办法是在启动参数里显式把网络打开:
mysqld --defaults-file="C:\ProgramData\MySQL\MySQL Server 8.0\my.ini" --skip-grant-tables --skip-networking=0 --console或者用共享内存的方式连接,客户端加--protocol=memory。我的习惯是直接加--skip-networking=0,简单直白。但这里有个安全提醒:跳过权限表且开启网络监听的实例,任何能访问到这个端口的人都可以无密码连入并拿走所有数据。所以这种状态只能维持几分钟,改完密码立刻停掉,绝对不要让它就这么跑着过夜,更不要在有公网入口的机器上这么干。这是我最想强调的一条,重置密码是个安全敏感操作,不是普通的配置调整。
2.3 路径二:用 init-file 在启动瞬间执行改密语句
如果你的场景是服务不方便长时间前台挂着,或者脚本化程度要求高,--init-file更合适。做法是先把改密语句写进一个文本文件,比如C:\mysql-init.txt:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewStr0ng#Pass';注意这个文件里不要写注释、不要写多条无关语句,一条改密语句最干净。然后用这个文件启动:
mysqld --defaults-file="C:\ProgramData\MySQL\MySQL Server 8.0\my.ini" --init-file="C:/mysql-init.txt" --console这里有个 Windows 特有的细节:路径里的反斜杠在参数中有被当转义符处理的风险,改用正斜杠最稳妥,或者写成双反斜杠。我第一次用这个方式时就是因为路径写成单反斜杠,服务端报找不到文件,排查了好一会儿。
--init-file的机制是服务端正常启动(权限表正常加载、网络正常监听),在启动过程中把文件里的语句执行一遍,执行完就可以把文件删了。所以它的好处是不需要跳过权限表,不存在无密码可连的窗口期,安全性比路径一高。代价是你得先确认这条 SQL 语法在当前版本上是对的——8.0 用ALTER USER,5.7 用ALTER USER或者SET PASSWORD都行。如果 SQL 写错,服务端会启动失败并把错误打到控制台,改对再来一次即可,不会损坏数据。
2.4 改完密码后的三件收尾事,一件都不能落
第一件:正常关闭这个临时实例。前台窗口直接按Ctrl+C是可行的,但更稳妥的方式是先在mysql会话里执行SHUTDOWN;(需要先FLUSH PRIVILEGES;),或者用taskkill /IM mysqld.exe /F强制结束。强制结束有极小概率留下未刷盘的脏页,不过刚改完密码这种场景基本无所谓。之后执行net start MySQL80把正常服务拉起来。
第二件:确认新密码真的生效。服务起来后,用新密码连接一次:mysql -u root -p -h 127.0.0.1 -P 3306。这一步别省,我见过改完没验证,第二天发现还是连不上,结果是改的是另一台机器上的实例。
第三件:清理临时文件和参数。把C:\mysql-init.txt删掉,确认my.ini里没有被你不小心写进skip-grant-tables(有些人图省事直接改配置文件,改完忘了删,那服务就永远是免密状态,这是很典型的安全事故源头)。my.ini里如果出现这一行,务必注释或删除。
补充一个 Windows 上更省事的选项:如果这台 MySQL 是用 MySQL Installer 装的,直接运行 Installer,选Reconfigure,走到账户设置那一步就能重设 root 密码,图形界面全程引导,风险最低。我一般在能开图形界面的桌面机器上优先用这个,只有在服务器或 Installer 已卸载的情况下才走命令行的两条路径。
3. 新建用户:'user'@'host' 是一整套匹配规则,不是一个名字
新手最容易把'appuser'@'localhost'当成"用户名 appuser"来理解,然后在业务服务器上连不上,回头查密码、查端口、查防火墙,查了一圈发现是 host 部分没配对。MySQL 的账号标识是"用户名 + 来源主机"的组合,两个部分都相同才算同一个账号,'appuser'@'localhost'和'appuser'@'%'是两条完全独立的记录,各自有各自的密码和权限。
3.1 host 部分的四种写法与它们的实际含义
| 写法 | 含义 | 适用场景 |
|---|---|---|
'appuser'@'localhost' | 仅本机来源 | 数据库和应用在同一台机器,最安全 |
'appuser'@'127.0.0.1' | 仅本机 TCP 回环 | 应用用 IP 连接而非 localhost 时用 |
'appuser'@'192.168.10.%' | 指定网段 | 内网应用服务器固定网段 |
'appuser'@'%' | 任意来源 | 来源不固定,权限上最宽松 |
匹配顺序上,MySQL 会按 host 的具体程度来定优先级,越具体的越先匹配。比如同时存在'appuser'@'localhost'和'appuser'@'%',从本机来的连接会命中前一条。这里有个历史遗留的讲究:某些版本和配置下%不一定能覆盖本机连接,为了少踩坑,我的做法是本机一条、远程一条,两条都建,密码保持一致,宁可多一条记录,也不要在这上面赌。
生产环境强烈建议不要用%。数据库端口一旦被非预期的来源访问到,%就是最后一道防线失守。用固定网段或者具体 IP 列表,麻烦一点,但出事的时候你会庆幸当初多写了那几个字符。
3.2 CREATE USER 的标准写法与密码策略冲突
8.0 的标准写法:
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'Str0ng#Pass2024';看着简单,但实际执行经常会撞上ERROR 1819 (HY000): Your password does not satisfy the current policy requirements。原因是 8.0 默认装了一个密码强度校验组件,要求密码同时包含大小写字母、数字、特殊符号,长度还要够。先看当前策略:
SHOW VARIABLES LIKE 'validate_password%';5.7 和 8.0 的变量名有差异(8.0 是validate_password.policy这种带点号的形式),看到具体值再决定是改密码还是改策略。改策略的命令是SET GLOBAL validate_password.policy = LOW;(8.0 写法),改完再建用户。
我的建议是优先改密码,而不是降策略。把策略调低是一时的方便,代价是以后所有账号都可能在弱密码状态。如果确实是在测试环境、只想赶紧跑通,那临时调低、跑完再调回来,也算可控。另外要注意SET GLOBAL只在当前运行期有效,重启服务后回到配置文件里的值,想持久化得改my.ini。
还有一个ERROR 1396 (HY000): Operation CREATE USER failed for 'appuser'@'localhost',含义是这条记录已经存在(或者存在同名的匿名用户冲突)。先SELECT user, host FROM mysql.user WHERE user='appuser';看一眼,确实存在就先DROP USER再重建,或者直接用ALTER USER改密码。
3.3 认证插件选错,是"能建用户但连不上"的头号原因
如果新建的账号自己用命令行能登、业务程序死活连不上,八成是插件问题。8.0 默认caching_sha2_password,一些老驱动不认。确认当前插件:
SELECT user, host, plugin FROM mysql.user WHERE user='appuser';需要兼容老客户端时,建用户时直接指定插件:
CREATE USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'Str0ng#Pass2024';已经建好的账号也能改:
ALTER USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'Str0ng#Pass2024';这里要提醒的是,较新的 8.x 版本里mysql_native_password插件的默认启用状态有过调整,某些版本默认不再加载。执行前用SHOW PLUGINS;看一眼这个插件在不在、状态是不是 ACTIVE,比照着老文章硬试要靠谱。如果插件确实不可用,正确方向是升级客户端驱动,而不是想办法把服务端降级——驱动升级通常只是换个依赖版本,服务端降级是动数据的事,两者风险不是一个量级。
4. 给新用户建库:字符集这件事现在不定,以后要还债
建库一行 SQL 就够,但这一行里省掉的参数,往往在半年后变成"为什么表情符号存进去变成问号""为什么排序结果和预期不一致"。我的习惯是建库时把字符集和排序规则写全,不依赖服务端默认值,因为默认值在不同版本、不同安装方式下并不一致。
4.1 CREATE DATABASE 里那几个参数到底在管什么
CREATE DATABASE appdb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;CHARACTER SET决定能存什么字符。utf8mb4是真正的四字节 UTF-8,能存表情符号和部分生僻字;MySQL 里那个叫utf8的字符集其实是三字节的utf8mb3,存不了四字节字符。这是历史遗留命名,8.0 已经开始把utf8逐步指向utf8mb4,但显式写utf8mb4永远不会有歧义。
COLLATE决定字符串怎么比较和排序。utf8mb4_general_ci是不区分大小写的通用排序规则,兼容性好;8.0 还提供了utf8mb4_0900_ai_ci,排序更符合现代 Unicode 规则,但只有 8.0 及以上支持。如果这套库将来可能要迁到 5.7,用utf8mb4_general_ci更保险;如果确定只在 8.0 上跑,用0900_ai_ci更规范。这一点在建库时就要定下来,因为后期改库的默认字符集不会自动改已有表的字符集,要一张表一张表地ALTER,数据量大的时候是个大工程。
顺便说一个反直觉的点:CREATE DATABASE不指定字符集时,会继承服务端的character_set_server,而这个值在 5.7 的 Windows 安装包里默认是 latin1。中文环境下这意味着什么不用多解释。所以第一次接手一台 MySQL,先执行SHOW VARIABLES LIKE 'character_set%';看一眼,比事后救火强。
4.2 命名约定:一人一库、前缀隔离、别用保留字
给用户配库的时候,我倾向于遵循几个简单的约定,纯粹是为了后来人省事。库名带业务前缀,比如app_order、app_user,避免多个业务共用一个实例时名字打架。库名全小写加下划线,因为 Windows 上文件名不区分大小写而 Linux 上区分,同一套脚本跨平台跑的时候,大小写混用会在迁移那一刻集中爆发。
库名不要用保留字(order、group、key、desc这些),一旦用了,之后每条 SQL 都得加反引号,写一次烦一次。实在要用,CREATE DATABASE \order`;` 加反引号也能建,但那是给自己找麻烦。
还有一个实操习惯:建库和授权用同一个脚本一次做完,不要建完库过两天再想起来授权。分开做的时候极容易漏掉某条权限,然后业务上线前一刻才发现缺个CREATE权限,现场改权限的风险比提前想清楚高得多。
4.3 建完库先别急着 GRANT,想清楚业务到底需要什么
在授权之前,先花两分钟回答一个问题:这个账号是给谁用的?如果是应用连接池用的账号,它通常只需要对自己那个库的增删改查和建表改表权限,不需要DROP DATABASE,不需要访问mysql系统库,更不需要FILE(这个权限可以读写服务器上的文件,属于高危)。如果是给开发同事临时查数据用的只读账号,那就只给SELECT,给多了迟早出事。
这个思考过程就是最小权限原则的落地。它不是什么高深的安全理论,就是一个朴素的判断:这个账号被入侵或者密码泄露时,攻击者能造成的最大破坏是什么?一个只有SELECT权限的报表账号泄露,损失是数据被读;一个ALL PRIVILEGES ON *.*的账号泄露,损失是整台实例。两者防范成本差不多,后果差一个数量级。
5. 授权与验收:语句写对只是及格,验证过才算完成
授权本身是几条GRANT的事,但真正区分"会操作"和"操作靠谱"的,是授权之后有没有做验证。我见过太多情况是权限授了、看着没报错,业务一跑就报Access denied,回头查发现是 host 不匹配、是权限给在了错误的库上、或者是GRANT之后没刷新权限表(虽然在新版本里绝大多数情况不需要手动刷新,但某些场景确实需要)。
5.1 GRANT 语句的四种颗粒度
-- 只给某个库的读写(最常见) GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'localhost'; -- 加上建表和改表能力(业务需要自己维护表结构时) GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP, REFERENCES ON appdb.* TO 'appuser'@'localhost'; -- 该库的全部权限(不含 GRANT OPTION,不能给别人授权) GRANT ALL PRIVILEGES ON appdb.* TO 'appuser'@'localhost'; -- 全局权限(危险,慎用) GRANT ALL PRIVILEGES ON *.* TO 'appuser'@'localhost';ON appdb.*表示库级权限,只作用于这一个库。ON appdb.order_table是表级,粒度更细但管理成本高,一般只在特殊场景用。ON *.*是全局,等于这台实例上的所有库。
GRANT OPTION是一个单独的开关,它允许被授权的用户再把这个权限转授给别人。给业务账号加上这个,等于让它成为一个小 root,除非确实有分级管理的需求,否则不要加。
执行完GRANT,跑一句FLUSH PRIVILEGES;是个保险动作。严格来说,用GRANT、CREATE USER、ALTER USER这些语句改权限时,服务端会自动重载权限表,不需要手动刷新。只有直接UPDATE mysql.user表这种老式做法才必须刷新。但加这一句没有副作用,新手加上它,能避免"到底是权限没生效还是我记错了"这种自我怀疑。
5.2 授权后怎么验证:SHOW GRANTS 加真实登录
第一层验证,看权限表怎么记的:
SHOW GRANTS FOR 'appuser'@'localhost';输出会明确列出这个账号有哪些权限、作用在哪个范围。如果输出里是GRANT USAGE ON *.* TO ...,说明这个账号除了能登录什么权限都没有,USAGE是个占位符,不代表任何实际权限。这种情况通常是CREATE USER建了用户但忘了GRANT,或者GRANT语句里的 host 部分和建用户时写得不一样。
第二层验证,用这个账号真实登录一次,并且换一个库试试:
mysql -u appuser -p -h 127.0.0.1 -P 3306 appdb登录成功后执行USE appdb;和一条简单的SELECT COUNT(*) FROM 某张表;,再执行一次USE mysql;确认它没有权限访问系统库。这一步很重要,很多人只验证了"能连上",没验证"权限边界对不对"。前者是功能,后者是安全。
第三层验证,如果是给远程应用用的账号,一定要从应用那台机器上测一次,不要只在数据库本机测。本机能连不代表远程能连,中间隔着 bind-address 配置、Windows 防火墙入站规则、云服务器安全组。ERROR 2003和ERROR 1045在这两个层面上的排查方向完全不同,先分清是"连不到端口"还是"连到了但被拒"。
| 报错 | 大致方向 | 优先排查 |
|---|---|---|
ERROR 2003 (10061) | 根本没连上服务 | 服务是否运行、端口、防火墙、bind-address |
ERROR 1045 (28000) | 连上了但认证失败 | 用户名、密码、host 匹配、认证插件 |
ERROR 1044 (42000) | 登录成功但无权访问该库 | 该账号对该库的 GRANT 是否生效 |
ERROR 1133 (42000) | 找不到匹配的用户记录 | GRANT前是否先建了用户 |
5.3 撤权、改密、锁定:账号生命周期里最容易被忽略的部分
权限给错了要能收回来,这比给出去更重要。撤掉某一项权限:
REVOKE DELETE ON appdb.* FROM 'appuser'@'localhost'; FLUSH PRIVILEGES;撤掉全部库级权限:
REVOKE ALL PRIVILEGES ON appdb.* FROM 'appuser'@'localhost';注意REVOKE之后账号本身还在,只是没了权限,SHOW GRANTS里会回到USAGE状态。要彻底删除账号用DROP USER 'appuser'@'localhost';,删除时同样要带完整 host,只写用户名会报错或者删错。
人员离职、项目下线这类场景,我更推荐先锁定再删除:
ALTER USER 'appuser'@'localhost' ACCOUNT LOCK;锁定的账号无法登录,但记录和权限都保留着,万一发现还有程序在用它,解锁就能恢复,排查完再删。直接DROP的话,某个还在跑的定时任务会立刻开始报连接错误,而你可能一周后才知道是它。
改密码就用ALTER USER ... IDENTIFIED BY,改完所有用到这个账号的应用都得同步更新配置,这一点在生产环境上要提前排期,别在业务高峰改。
5.4 一份可以直接抄的初始化脚本
把前面的步骤合成一份可重复执行的脚本,测试环境里我很喜欢这种写法:
-- 1. 建库(字符集显式指定,不依赖默认值) CREATE DATABASE IF NOT EXISTS appdb DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- 2. 建用户(本机 + 远程各一条,密码自行替换) CREATE USER IF NOT EXISTS 'appuser'@'localhost' IDENTIFIED BY 'Str0ng#Pass2024'; CREATE USER IF NOT EXISTS 'appuser'@'%' IDENTIFIED BY 'Str0ng#Pass2024'; -- 3. 授权(只给这个库的业务权限,不含 DROP DATABASE、不含全局) GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES ON appdb.* TO 'appuser'@'localhost'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES ON appdb.* TO 'appuser'@'%'; -- 4. 生效并核对 FLUSH PRIVILEGES; SHOW GRANTS FOR 'appuser'@'localhost';用IF NOT EXISTS的好处是脚本可以反复跑,不需要先判断存不存在。代价是如果账号已存在,密码不会被更新——这在"新建"场景是优点,在"重置密码"场景是坑,得配合ALTER USER单独处理。这一点心里要有数。
6. 那些年踩过的坑,以及一份能救命的排错清单
前面讲的都是正向流程,但实际操作里让人卡住的往往是异常状态。这一节把几个高频报错和它们的真实成因拆开讲,附上我自己定期会用到的几条命令,你可以存下来当卡片用。
6.1 为什么 skip-grant-tables 状态下改密码会报 1290
执行ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码';时返回:
ERROR 1290 (HY000): The MySQL server is running with the --skip-grant-tables option so it cannot execute this statement很多人看到这个会以为跳过权限表的方式不可行,转而去试别的野路子。实际上服务端的意图很明确:权限表都没加载,改账号信息没有意义,所以拒绝执行。解决办法是先让服务端把权限表加载进内存:
FLUSH PRIVILEGES;执行完这一句,当前会话就获得了完整的权限管理能力,再执行ALTER USER就正常了。这个顺序是固定的,先FLUSH PRIVILEGES,再改密码,反了就是 1290。老教程里写的UPDATE mysql.user SET password=PASSWORD(...)之所以能在 5.7 上跑通,是因为它直接改表,绕过了语法层面的限制,但 8.0 里这个字段和函数都没了,那条路彻底堵死。
6.2 服务起不来:端口占用、data 目录、配置残留
net start失败的时候,别急着重装,先看错误日志。日志位置通常是 data 目录下的主机名.err,data 目录可以用sc qc查到配置后再去my.ini里找datadir。几个高频原因:
端口被占用。上一个mysqld进程没退干净,或者前台窗口忘了关,新服务起来时端口 3306 已被监听。查一下:netstat -ano | findstr :3306,拿到 PID 后tasklist | findstr 进程号确认是不是 mysqld,是的话taskkill /PID 进程号 /F。这个坑我在重置密码流程里踩过不止一次,因为前台启动的那个mysqld特别容易被忘掉。
data 目录权限不对。用 ZIP 包解压的实例,如果 data 目录是在普通用户权限下创建的,之后再以服务方式(SYSTEM 账户)启动,可能因为没有写权限而失败。检查方式是在my.ini里找datadir,右键属性看安全选项卡里 SYSTEM 有没有完全控制权限。
配置残留。曾经为了重置密码往my.ini里加过skip-grant-tables,后来忘了删。表现是服务能起来,但任何密码都能登,看起来"没有报错",实际上安全防线已经没了。定期用findstr /I "skip-grant" 配置文件路径检查一遍,是个成本极低的好习惯。
6.3 几条我个人的习惯,纯经验
重置密码前先备份 data 目录。不是每次都备,但如果这台机器上的数据没有其他副本,而这个操作又涉及停服务、改配置,花十分钟复制一份 data 目录,是性价比最高的保险。跳过权限表本身不动数据,但人在紧张状态下敲错命令的概率会上升,而--initialize这类命令是不可逆的。
用脚本而不是手敲。建库、建用户、授权这三段 SQL,我会写成一个.sql文件用mysql < init.sql执行,而不是在mysql>里一条条敲。原因是手敲容易漏;、容易把 host 写错一个字符,而脚本可以 review、可以进版本库、可以在另一台机器上重放。对一个需要长期维护的项目来说,这份脚本本身就是文档。
给账号加上用途注释。8.0 的CREATE USER支持COMMENT参数:CREATE USER 'report_ro'@'%' IDENTIFIED BY 'xxx' COMMENT '报表只读账号,联系人见wiki';。半年后你看到一堆账号,能靠注释快速判断哪个能删、哪个动不得,这个投入产出比高得离谱。
定期跑一次账号盘点。
SELECT user, host, plugin, account_locked FROM mysql.user ORDER BY user, host;看看有没有不该存在的%账号、有没有长期没用的空密码账号、有没有锁定了但其实早该删的账号。这件事做一次十分钟,能避免很多说不清的隐患。
最后分享一个我在 Windows 上特别依赖的小技巧:把mysql.exe所在目录加进系统 PATH 之后,所有命令都能直接写mysql -u xxx -p,不用每次敲一长串带引号的完整路径。但要注意,如果机器上有多个版本,PATH 里排前面的会先命中,加之前先确认顺序,否则会出现"我明明改了 8.0 的用户,怎么连上去还是 5.7 的行为"这种迷惑现象。我一般是在脚本里写完整的mysqld.exe路径,只有在交互式排查时才依赖 PATH,两套方式各管一段,出错时定位起来也清楚。