如果你接手过一个团队项目,多半遇到过这样的场景:业务刚起步时,DBA(数据库管理员)懒得细分,所有人都拿一个 root 账号连数据库。开发、测试、运维共用同一个超级账号,今天谁误删了一张表,明天谁改错了一行配置,出了事故都查不出是谁干的——因为大家用的账号、密码是同一个。更麻烦的是,root 账号一旦泄露,等于把整台数据库服务器的大门钥匙交给了陌生人,谁都能用最高权限对你的机器和数据为所欲为。

这就是"用户管理"存在的意义:让不同的人用不同的账号,给不同的账号配不同的权限,把"谁能从哪里、用什么权限进来"这件事讲清楚。MySQL 的用户管理远不止"建个账号、设个密码"这么简单,它背后藏着一套完整的主机识别逻辑、一个层级分明的权限体系,还有一堆哪怕工作了几年的人也容易踩的坑——host 通配符、直接改表后的权限不生效、远程访问配置、新旧认证插件的兼容性,等等。

这篇文章我们就把 MySQL 的用户、权限、字符集和备份这些运维关键话题一次讲透。你会先认识用户到底存在哪里、host 起着什么样的作用,然后跟着我一步步学会创建用户、删除用户、修改密码,再深入权限体系,理解 GRANT 授权、REVOKE 回收、权限级别和 FLUSH PRIVILEGES 的生效时机。最后,我们还会聊到字符集的正确配置,以及一套能救命的备份与恢复操作。每段都配了可以直接照着敲的命令示例,末尾还有带详解答案的思考题。

为什么不能只用 root 一个账号

先用一句话回答开头的追问:只用 root,安全隐患太大。这里的 root 是 MySQL 默认创建的超级管理账号,它默认拥有对整个 MySQL 实例(instance,也就是当前这台服务器上运行的那套 MySQL 服务)的所有权限,包括建删库、改任何表、停止/重启服务等。把这个账号到处共享,等于把整个家门的钥匙印了很多份发下去,还一视同仁地批发给了所有人。系统的安全设计原则里有一条叫"最小权限原则"(Principle of Least Privilege):每个账号只给它完成自己工作所必需的最少权限,不多给一分。MySQL 用户管理,正是落地上这条原则的工具。

日常开发里,一个人往往同时是多个角色:有的人只需要读数据(SELECT),有的人需要写数据(INSERT/UPDATE/DELETE),有的人要建表、删表(DDL),还有的人负责运维(CREATE USER/RELOAD)。把这些职责拆到不同的账号上,好处是显而易见的:

  • 可追溯:每笔操作都知道是谁做的,出了事故能顺着账号找到责任人;
  • 可收敛:某个账号权限给多了,收回它一个就行,不影响别人;
  • 可隔离:一个泄露的普通账号,顶多损失那部分权限,不至于连累整个库。

所以 MySQL 用户管理要做的事,本质上就是三件:识别登录者(是谁)→ 验证登录者(密码对不对)→ 控制登录者能干什么(权限够不够)。接下来我们就从"用户存在哪"讲起。

用户信息:mysql 系统库与 user 表

你大概会好奇:MySQL 里那些用户,到底是存哪儿的一份"花名册"?答案是:MySQL 把用户的账号信息集中存在一个特殊的库里——系统的 mysql 数据库(也叫系统库或元数据库,专门存放 MySQL 运行所需的元数据,包括用户、权限、事件、时区等)。

而记录用户的核心表,是 mysql 库下的 user 表。一张表里,每条记录就对应一个用户账号(更准确地说,对应一个"用户名 + 主机名"的组合,这一点后面细讲)。你可以像查普通表一样去查它:

-- 进入 mysql 系统库
use mysql;
-- 查看所有用户的用户名、主机、以及加密后的密码串
select host, user, authentication_string from user;

user 表里几个关键字段的含义我们先说清楚,它们是理解整个用户体系的钥匙:

  • host:表示这个用户可以从哪台主机登录。如果是 localhost,表示只能从本机(也就是 MySQL 服务器自己那台机器)登录;如果是某个 IP,比如 192.168.1.100,表示只能从那个 IP 的机器登录;如果是 %,表示可以从任意主机登录。
  • user:用户名,就是登录时输入的那个名字。
  • authentication_string:用户密码经过加密后得到的密文。注意,它存的不是明文密码,而是经过哈希(你可以简单理解成"单向搅乱后的指纹",只能由密码算出来,却无法反推出密码)处理的串。打开表看到的是一长串看似乱码的值,那正是密码的密文痕迹。
  • *_priv:以 priv 结尾的一堆字段,表示该用户当前拥有的各项权限(privilege,即"被授予的操作权利")。比如 Select_priv、Insert_priv、Update_priv、Delete_priv 等,值为 Y(享有)或 N(不享有)。

一个小提醒:在较老版本的 MySQL(5.6 及更早)里,密码字段叫 password 而不是 authentication_string;从 5.7 起才改名为 authentication_string。所以网上老教程里写 select password from user;,在你较新版本的库里是会报"字段不存在"的。

系统启动时,MySQL 会把 user 表的内容一次性读进内存中的授权缓存(access control cache,权限判断时优先读内存里的这份副本,性能好)。所以你要留意一个极其重要的纪律:正常改动账号权限,都要用 MySQL 提供的专门语句(如 CREATE USER / GRANT / REVOKE),而不要直接手工去 insert / update / delete 这张表。为什么?因为直接改表,改的只是磁盘上的数据,内存里那份缓存并不会自动跟着更新——于是就会出现"明明改了表,权限却不生效"的怪现象。这条坑我们后面讲到 FLUSH PRIVILEGES 时会再回头仔细展开。

host:你的用户从哪里来

很多新手卡在 '用户名'@'主机名' 这个写法上,觉得多此一举。请你一定记住 MySQL 一条核心规则:在 MySQL 里,一个用户是由"用户名 + host"两者共同唯一确定的,而不是单看用户名。

也就是说,'whb'@'localhost' 和 'whb'@'%' 在 MySQL 眼里是两个完全不同的账号。前者只允许在服务器本机登录,后者允许从任何主机登录。哪怕它们同名、同密码,权限也可以完全不同。这就像同一个工牌号,但"在本栋楼进出"和"在全国各地进出"是两套权限。

host 值的写法很有讲究,有几种常见形式:

host 值含义
localhost只能从本机(Unix 套接字 / 本机回环)登录
127.0.0.1只能从本机回环地址经 TCP 登录(注意和 localhost 有时候不完全等价,下面讲远程访问时细说)
192.168.1.100只能从该具体 IP 登录
192.168.1.%只能从 192.168.1.x 这个网段登录(% 是通配符,代表任意)
%可以从任意主机登录

这里的 % 就是 MySQL 用户系统里最重要的通配符,它匹配"任何主机"。注意通配符不仅能用在整个 host 上('%'),也能用在 IP 的一部分上(比如 '192.168.1.%',匹配该网段下面所有机器)。

关于 %,有三条你务必警惕的坑:

坑一:% 并不匹配 localhost。 这是一个特别反直觉的点。'whb'@'%' 虽然号称"任意主机可登",但它不含 localhost。当你从服务器本机去连(走本机套接字或 127.0.0.1)时,MySQL 会优先用 'whb'@'localhost' 这个账号来匹配;如果只建了 'whb'@'%' 而没有 localhost 账号,本机直连反而可能登不进去。所以一个稳妥的做法往往是给本机单独建 '用户'@'localhost',再按需给远程建带 IP 或 % 的账号。

坑二:不要随手建一个 '%' 的宽泛账号。 源文档里特别强调过这句"不要轻易添加一个可以从任意地方登陆的 user",这是很强的实战忠告。'whb'@'%' 意味着世界上任何一台机器的客户端,只要有这个账号密码,就能尝试连你的库。一旦密码泄露,攻击面是"全世界",损失不可控。除非确有需要(且配套防火墙等手段),否则宁可用具体 IP 或网段,把 host 收窄。

坑三:host 相同但用户名不同,是不同账号;用户名相同但 host 不同,也是不同账号。 二者交叉排列都能各算一个账号,别混淆。

做个思想实验帮你巩固:为什么本地装完 MySQL 后,root 往往要配成 'root'@'localhost' 而不是 'root'@'%'?正因为它只允许本机连,从而把超级账号的暴露面压到最小。这也是为什么源文档里的示例表里,常见 root @ localhost 或 root @ % 两种形态,它们针对的是不同的登录场景。

创建用户

认识了用户表,接下来就是真正"建档"。创建用户的标准语句是 CREATE USER:

-- 创建一个只能从本机(localhost)登录、用户名为 whb、密码为 12345678 的账号
create user 'whb'@'localhost' identified by '12345678';

逐句拆开看:

  • create user:创建用户的关键字;
  • 'whb':用户名,用单引号包起来;
  • '@localhost':主机限制,表示这个账号只能从本机登录(前文说过,'用户名'@'host' 是一个完整账号标识);
  • identified by '12345678':identified by 是"用……作为身份凭证"的意思,后面跟的是明文密码。MySQL 会帮你把它加密后存进 authentication_string,你永远不用在 SQL 里写密文。

创建完之后,你可以再查一次 user 表,会看到多出一行 whb:

-- 创建用户之后,再查一次用户表确认多了新行
select user, host, authentication_string from user;

新建的 whb 账号登录时什么权限都没有(只有最基础的 USAGE,也就是"能连上、能空跑"的能力),需要有权限再 GRANT,我们后面细讲。

创建完,用新账号就能登录了。登录命令是在操作系统终端里敲的,不是 SQL:

# 在服务器本机用新账号登录,-u 指定用户名,-p 会提示输入密码
mysql -u whb -p

创建用户时的两个常见报错,必须提前告诉你:

报错一:密码强度不满足策略。 当你设一个过于简单的密码(比如 123456),MySQL 可能会报:

ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

意思是"你的密码不满足当前的密码策略要求"。这是因为较新版本的 MySQL 默认开启了一个密码强度插件(组件)validate_password,它对密码的最小长度、包含的字符种类等有要求。想看当前策略长什么样,可以执行:

-- 查看密码强度相关的参数(最小长度、强度等级等)
show variables like 'validate_password%';

处理办法有两个方向:要么把密码设得足够强壮以通过校验;要么(仅在自己本地测试环境)临时把策略调低——这里还是强烈建议你保持强密码习惯,别在真环境里为省事关掉它。

报错二:账号名与主机组合不匹配。 比如你想删 'whb'@'localhost' 却只写了 drop user whb;,会报 ERROR 1396,我们下面删用户那节详细讲。

顺带一提:在部分 MySQL 出台"密码策略"相关组件后,如果安装时选择的是比较严格的策略,即使像 'whb'@'localhost' identified by '12345678' 这种 8 位纯数字,也可能撞上上面的 1819 报错。遇到时先查策略,按策略提强度要么改密码,要么酌情调整策略参数。

提一句 CREATE USER 的权限要求

要创建用户,你自己得有 CREATE USER 权限(典型是 root 或 DBA)。如果你是一个普通用户却去执行 create user,会得到 Access denied 类的报错。这也是"给账号配权限"的闭环观:用什么权限干活,就需要先被授予那个权限。

修改用户密码

用户创建之后,密码难免要改。改密码有两种场景:自己改自己的密码,和 root(或管理员)给指定用户改密码。

自己改自己的密码

语法(现代 MySQL 推荐,8.0 里尤其如此):

-- 修改自己的密码为新的密码,后面直接写新密码明文
set password = '新密码';

root 给指定用户改密码

-- root 给 whb@localhost 把密码改成 87654321
set password for 'whb'@'localhost' = '87654321';

逐句拆:set password for 后面跟一个完整的 '用户名'@'主机名',再 = 接新密码明文。

重要版本差异提示:老教程里常写成 set password = password('新密码');,这里的 password('...') 是一个把明文加密成密文的函数。但在 MySQL 5.7 里这个 PASSWORD() 函数已被标记为废弃(deprecated),到 MySQL 8.0 更是直接移除了。所以你在 8.x 上照抄那套会报"函数不存在"之类的错。现代版本直接写 set password = '新密码'; 即可,MySQL 会自动用合适的加密算法去处理。如果你在教程或旧项目里看到 password() 这种写法,心里要有数:那是老版本的遗留风格。

另外,MySQL 8.0 之后更推荐的改密码姿势其实是用 ALTER USER:

-- 用 ALTER USER 修改用户名和主机对应的账号密码(8.0 更推荐的写法)
alter user 'whb'@'localhost' identified by '新密码';

ALTER USER 语义上更清晰,一步到位。你可以把 SET PASSWORD 和 ALTER USER 都掌握,遇到不同版本都能应付。

改完密码后,账号原来的登录会话可能会因为认证信息变化而受影响,一般重新登录即可用新密码。

删除用户

要"注销"一个账号,用 DROP USER:

-- 删除 whb@localhost 这个账号
drop user 'whb'@'localhost';

这里必须特别强调host 参数不能省,它正是本课最容易被坑的地方之一。来看一个典型翻车现场:

-- 错误示范:只给了用户名,没给 host
drop user whb;

执行后你会得到:

ERROR 1396 (HY000): Operation DROP USER failed for 'whb'@'%'

为什么?因为当你只写一个裸用户名 whb,MySQL 会把 host 默认补成 '%',也就是它尝试删除 'whb'@'%' 这个账号。可你系统里实际存在的是 'whb'@'localhost',于是它找不到要删的目标,干脆报错。记住了:写账号名时,域名部分(host)能省,但删用户、改密码时为了精确锁定目标,最好带上完整 '用户名'@'主机名'。 这跟前面说"用户名+host 才唯一确定一个账号"完全呼应——你想要删除的对象必须是完整的账号,而不是一个含混的名字。

删除后再查 user 表,那一行就消失不见了。

权限体系:为什么"有了账号还得授权"

创建完一个空账号你会发现:它能连上来,但什么都干不了——show databases 看不到你业务库,select 必然被拒。这是因为MySQL 默认不给你任何对象权限(只有基础的 USAGE,连上去空跑没问题)。要想让它真正干点活,就得"授权"。

MySQL 有一套层级分明的权限体系,理解它,你就理解了 GRANT 里的 ON 那部分到底在说哪个范围。权限大致分这几级(范围从大到小):

  1. 全局级(*.*):作用在整台 MySQL 实例上,即所有数据库的所有对象都包含在内。
  2. 数据库级(db.*):作用在某一个数据库名下的所有对象。
  3. 表级(db.表名):只作用在某一张表上。
  4. 列级:甚至能细到只给某一张表里的某一列权限。
  5. 子程序级:作用在存储过程、函数等对象上。

其中 *.* 和 库名.* 是最常用的两种写法,它们的含义分别是:

  • *.*:本系统中所有数据库的所有对象(表、视图、存储过程等);
  • 库名.*:某个数据库中的所有数据对象。

这俩看着像,但一个是"全景",一个是"单库"。给错级别,要么权限给太大(危险),要么权限不够(没用)。日常给业务用户授权,最典型的粒度就是给它业务所在的库,而很少给 *.*。

GRANT 授权

"授权"的英文是 grant,SQL 里就用 GRANT 这个关键字。基本语法:

grant 权限列表 on 库.对象名 to '用户名'@'登录位置' [identified by '密码'];

逐段拆解:

  • grant:授权关键字;
  • 权限列表:要授予的权限,多个并列权限用逗号隔开,例如 select, insert, update;也可以一口气给 all / all privileges,表示"该对象上的所有权限";
  • on 库.对象名:限定权限作用的范围和对象。test.* 表示 test 库下所有对象,*.* 表示所有库所有对象,test.account 则表示 test 库的 account 这一张表;
  • to '用户名'@'登录位置':授给哪个账号,登录位置 即 host;
  • [identified by '密码']:可选。它的作用是:如果该用户已存在,则在授权的同时顺带把密码改掉;如果该用户还不存在,就相当于"顺手把这个用户也创建了"。所以老版本的 GRANT 无形中兼任了"创建用户"的功能。

又一个版本差异提醒:上面 GRANT ... IDENTIFIED BY '密码' 这种"授权同时顺带创建/改密码"的写法,是 MySQL 5.7 时代的遗留习惯。到 MySQL 8.0 以后,GRANT 语句里不再支持 IDENTIFIED BY 子句,再这么写会直接报语法错误。8.0 的正确姿势是:先 CREATE USER(或者 ALTER USER)把账号和密码处理好,再用 GRANT 单独授权。这其实是更清晰的分工——"建账号"和"给权限"是两件事,别再混着来。

来看源文档里的完整案例。假设我们想给 whb 用户授予对 test 这个数据库下所有表的 SELECT(查询)权限:

-- 用 root 身份,给 whb@localhost 授予 test 库下所有对象的查询权限
grant select on test.* to 'whb'@'localhost';

授权之后,我们用 whb 账号连上,再看 show databases,就会发现之前看不到的业务库 test 出现了;进到 test 库,SELECT 查询也能顺利跑通。而一旦尝试删除数据,就会被拒绝:

-- whb 只有 select 权限,这里想删整表数据会被 MySQL 拒绝
delete from account;

会得到类似这样的报错:

ERROR 1142 (42000): DELETE command denied to user 'whb'@'localhost' for table 'account'

ERROR 1142 那句翻译过来就是:whb@localhost 这个用户没有对 account 表的 DELETE 权限。系统用这种方式把"我能看"和"我能改"清楚地隔开了——这也正是权限管理要达到的效果。

权限的组合写法也很常用,比如一次给多个读写权限:

-- 一次授予 select(查)、insert(插)、update(改)、delete(删)四个权限
grant select, insert, update, delete on brand_db.* to 'app'@'192.168.1.%';

这里的 app 用户可以对本网段的机器开放那四个基本读写操作,用于日常业务,而无需建表删表的大权限——这就是最小权限原则的落地。

查看账号的权限

想确认某个账号到底有什么权限,用 SHOW GRANTS FOR:

-- 查看 whb@localhost 的授权情况
show grants for 'whb'@'localhost';

输出会像这样:

Grants for whb@localhost
GRANT USAGE ON *.* TO 'whb'@'localhost'
GRANT ALL PRIVILEGES ON `test`.* TO 'whb'@'localhost'

这里第一个 GRANT USAGE 表示它只有最基础的连接能力(USAGE 就是"能连上来但没实际对象权限"的最低权限);第二个表示对 test 库下所有对象拥有全部权限。行内 WITH GRANT OPTION 如果出现,则表示该用户还能把权限再转授给别人(见下文"能转授的权限")。

顺带说明一个细节:show grants for 'whb'@'localhost'; 如果 localhost 这个 host 版本不存在而只存在 'whb'@'%',有的版本会提示没有该账号。所以查询时最好精确匹配你创建时的完整 '用户名'@'host'。

能转授的权限

MySQL 里有一类非常危险的授权附加值,叫 GRANT OPTION(转授权限)。默认情况下,A 授权给 B 的权限,B 无权再转给 C;但如果你在 GRANT 末尾加了 with grant option,就意味着"不仅给你这份权限,还允许你把这份权限再往下转授给别人"。

-- 给 whb 授 all 权限,并且允许 whb 再把权限转授给别人
grant all privileges on *.* to 'whb'@'localhost' with grant option;

with grant option 是把"代理权"也下放了出去,权限链条会越拉越长、越来越难管理。所以除非你真的需要"二级授权",否则不要加它。尤其在 *.* + WITH GRANT OPTION 的组合下,等于你造了第二个小 root,风险极大。源文档里 root 的授权就带 WITH GRANT OPTION,因为 root 本就是超级管理员,这是合理的;但对业务账号,轻易别给。

为什么直接往 user 表 insert 权限字段很危险

前面我们反复说"别直接改 user 表",这里正式解释原理。MySQL 在启动时把 grant 表读进内存授权缓存;之后你每次判断某用户有没有某权限,走的是内存缓存(快),而不是磁盘上的表文件。如果你 update mysql.user set Select_priv='Y' where ...,改的是磁盘上的数据,内存缓存并不知道。于是:

  • 权限判断看起来"不生效",改了半天 user 表白改了;
  • 一旦 flush privileges 或重启,服务器从磁盘重载——你手改的东西才突然"生效"。

所以正常途径永远是 GRANT / REVOKE / CREATE USER 这些账户管理语句——它们会自动把"磁盘"和"内存"两边同步更新。这也是慕课风格教程里会一直强调"不要手改授权表"的根源。

刷新权限:FLUSH PRIVILEGES

现在我们终于可以正面解释 FLUSH PRIVILEGES(刷新权限)这个命令了:

-- 让 MySQL 从磁盘授权表重新加载权限到内存缓存
flush privileges;

它本质是一句"让我重新读一遍授权表"的指令,把你可能手动改过、或机器里缓存过期的权限数据重新同步进内存。

关键结论(务必记住):执行 GRANT、REVOKE、CREATE USER、DROP USER、SET PASSWORD、ALTER USER 这些专属账户管理语句时,MySQL 内部会自动更新内存缓存,根本不需要你再手动 flush privileges。网上很多老文章说"授权之后必须 flush 一下才生效",其实在现版本里是多余的——GRANT 一执行,内存已经跟着变了。

那么什么情况下真的需要 flush privileges?只有当你绕过专属语句、直接用 insert / update / delete 去改授权表的时候(虽然我们强烈不建议这么做)。因为这时磁盘和内存脱节,你如果不 flush(或重启服务),改动就"悬空"着不生效。换句话说:

GRANT/REVOKE 之后不用 flush;手改授权表之后才用 flush。 而"手改表"这件事本身,才是真正不该做的。

把它记牢,你就不会像很多新手那样,明明只用 GRANT 却还在后面傻乎乎补一句 flush privileges;——用了也不算错,但完全没必要,而且会显得你不懂原理。

权限的"立即生效时机"

引申一个问题:我 GRANT/REVOKE 之后,正在线的用户什么时候能体验到变化?MySQL 官方的说法是分层的:

  • 全局级权限(*.* 上的权限)、密码变更:对已经建好的连接不会立刻生效,一般要到使用者下次新建连接时才会体现;新连接则直接使用新权限。
  • 数据库级权限(db.* 上的权限):在客户端执行下一个 use 数据库名;(或数据库相关的下一个请求)时生效。
  • 表级 / 列级权限:在客户端的下一次请求时生效。

所以源文档那个例子很贴合实际:root 在终端 A grant select on test.* 后,终端 B 里已登录的 whb 一开始 show databases 还看不到 test,等了一会儿(服务器重载了 db 权限,或它发了新的请求)才看到 test 库出现。这提醒我们两件事:一是权限生效有微小延迟,别急着断言"没生效";二是排查"权限没生效"问题时,先确认它是不是"正在会话的旧缓存"导致,让用户重连或发新请求往往就能解决。

REVOKE 回收权限

有授予就有回收。"回收权限" 用 REVOKE:

revoke 权限列表 on 库.对象名 from '用户名'@'登录位置';

它和 GRANT 几乎是镜像的语法,把 to 换成 from 语义就反过来了。逐段拆:

  • revoke:回收关键字;
  • 权限列表:要收回的权限,多个用逗号隔开,也可以写 all / all privileges(收回在该对象上的所有权限);
  • on 库.对象名:和授权时的范围对应;
  • from '用户名'@'登录位置':收回哪个账号的这些权限。

来看源文档的案例,root 把之前授予 whb 对 test 库的全部权限收回来:

-- root 身份,回收 whb 对 test 库的所有权限
revoke all on test.* from 'whb'@'localhost';

执行后,我们再拿 whb 账号连接,show databases 里那个 test 库就会消失(对它而言,库又变成不可见的了),自然也就无法再查表。

REVOKE 和 GRANT 一样,属于账户管理语句,执行后内存自动更新,同样不需要手动 flush。它回收的粒度也可以很细——只收 select、只收某张表的权限,都按同一套 on 范围语法来写。

REVOKE 要注意的坑

  • "回收要说清楚对象":revoke all on test.* 和 revoke all on *.* 回收的是两个不同层级的东西。你想收的是"它对 test 库的权限",就得用 test.*;如果用 *.*,收掉的是它全局的权限——很可能根本没授过,白收一遍还没达到预期。所以写 on 范围时,要和当初 GRANT 时保持对齐。
  • 回收的只是"当时授的":REVOKE 收的是"这回在 on 范围内授给它的那份权限",没授的不受影响。这也再次体现了"授权要记录清楚"的价值。

字符集:让中文不再乱码

前面都在讲"谁能进、能干什么",现在换个话题——字符集。它跟用户管理没直接关系,但却是数据库运维里几乎回回都要踩的坑:往库里插中文,读出来变成一堆问号 ? 或乱码,十有八九就是字符集没配对。

字符集(character set) 可以理解为"一套把字符映射成二进制编码的规则",比如某个字符在某种字符集里对应哪个字节序列。排序规则(collation) 则是"在某种字符集下,怎样比较字符大小"的细则,一般跟着字符集一起出现。

MySQL 里和字符集相关的三样东西最容易让人混淆:服务器字符集、数据库(库级)字符集、表/列字符集。它们有一个继承层级:设了服务器级,新建库默认继承;设了库级,新建表默认继承;设了表级,新建列默认继承。想查看当前各级字符集,运行:

-- 查看与字符集相关的所有系统变量
show variables like 'character_set%';

其中几个常看的关键项:

  • character_set_server:服务器默认字符集;
  • character_set_database:当前数据库的默认字符集;
  • character_set_connection:客户端连接使用的字符集。

而在会话中临时设字符集,最常用的是 SET NAMES:

-- 让本次连接用 utf8mb4 字符集与服务器交互(最推荐)
set names utf8mb4;

SET NAMES 字符集; 的语义是"我这个客户端,用指定的字符集跟服务器打交道",它会把你连接的 character_set_client、connection、results 一起设过去,是解决"中文乱码"最经典的临时手段。

为什么是 utf8mb4 而不是 utf8

说到这儿必须讲清一个著名的坑:MySQL 里的 utf8 其实并不是真正的全量 UTF-8,它只是 3 字节编码的 utf8mb3。而真正的、能覆盖全部 Unicode(包括 emoji 表情等 4 字节字符)的完整 UTF-8,在 MySQL 里叫 utf8mb4(mb4 = max bytes 4,即"最多 4 字节")。

如果你用 utf8,一旦遇到 emoji 或有特殊汉字的 4 字节字符,就会存不进去、读出来变 ? 甚至报错。所以 MySQL 官方如今也反复呼吁:新库新表,一律用 utf8mb4,别再选那个有坑的 utf8。

在建库、建表时显式指定字符集:

-- 建库时显式指定 utf8mb4 字符集,配套常见的中文排序规则 utf8mb4_general_ci
create database brand_db default character set utf8mb4 collate utf8mb4_general_ci;
-- 建表时显式指定字符集
create table user (
    id int primary key auto_increment,
    name varchar(50)
) character set utf8mb4 collate utf8mb4_general_ci;

collate 指定的是排序规则,utf8mb4_general_ci 是一套较适合中文场景的规则(ci = case insensitive,忽略大小写)。

如果库已经建好了想改默认字符集,可以用 ALTER:

-- 修改某个数据库的默认字符集为 utf8mb4
alter database brand_db character set utf8mb4;
-- 修改某张表的字符集为 utf8mb4
alter table user character set utf8mb4;

注意:alter table ... character set 只改"表的新建列的默认字符集"以及它声明的表级默认值,并不自动转换已有列的数据编码。要真正把已有列的数据也转成 utf8mb4,往往要 alter table xx convert to character set utf8mb4;(会重写整表,大数据量时开销较大)。初学阶段先记住"建库建表就用 utf8mb4,从源头上避免乱码",是性价比最高的习惯。

练习客户端乱码时,先 set names utf8mb4; 看是否解决,再检查连接工具、库、表的字符集设置,基本能覆盖 95% 的中文乱码问题。

远程访问:让别的机器连上来

很多时候,业务服务器和数据库服务器不是同一台机器,你需要允许从其他主机远程连接。远程访问要配通,其实要同时满足四件事,任何一环漏掉都连不上:

第一,用户的 host 要放行远程来源。 前文说过,'whb'@'localhost' 只允许本机。要让某个 IP 的机器连进来,就得用对应该 IP 或网段、或 % 的账号:

-- 允许 whb 从 192.168.1.100 这台机器登录
create user 'whb'@'192.168.1.100' identified by '强密码';
-- 或者放宽成整个 192.168.1.x 网段(% 作通配符)
create user 'whb'@'192.168.1.%' identified by '强密码';

第二,给该用户授上它需要的权限(GRANT),否则连上来也没权限干活。

第三,MySQL 服务要监听在网络接口上,而不是只监听本机。 有一个气死人的隐藏坑:默认情况下 MySQL 可能只 bind-address(绑定地址)到 127.0.0.1,也就是只在本机回环上监听,外界根本连不进来。想监听所有网卡,需要把 my.cnf 里的 bind-address 设为 0.0.0.0。在终端里查当前绑定情况:

# 查看 MySQL 是否在对外监听(Windows 用 netstat -ano | findstr 3306)
netstat -an | grep 3306

然后,客户端从远程连接时,用 -h 指定服务器 IP,并显式用 TCP 来连:

# 从远程机器上连服务器的 3306 端口,-h 后面跟服务器 IP
mysql -h 192.168.1.10 -P 3306 -u whb -p

第四,防火墙要放行 3306 端口。 服务器端防火墙如果挡着 3306,照样连不上。这是运维配合项,需要系统管理员确认。

远程访问还有一个概念坑:localhost 和 127.0.0.1 在 MySQL 里并不完全等同。用 mysql -u root -h localhost 连的时候,走的往往是本机套接字(socket);而 mysql -u root -h 127.0.0.1 走的是 TCP 回环。这两条路径匹配的 host 账号可能不同,进而出现"一个能连、一个连不上"的怪象。排查远程问题时要想到这一层。

给远程访问一个安全建议:能用具体 IP / 网段就不要用 %;必要时刻配合 IP 白名单(防火墙)收窄来源。远程开放 3306 等于把数据库门开到了网络上,务必在最小可用的前提下收敛。

authentication_string:mysql_native_password vs caching_sha2_password

讲密码改密时,我们没深入插件,但其实密码加密方式跟"新旧客户端连不上"这条坑有直接关系,值得单独讲清楚。

authentication_string 里那些密文,不是随便哈希出来的——它是用某种**认证插件(authentication plugin)**算的。较新版本 MySQL 里最常见的两种插件是:

  • caching_sha2_password:MySQL 8.0 的默认插件,安全强度更高。它采用 SHA-2 系列算法,并引入一个缓存机制提高常用密码的校验速度。
  • mysql_native_password:MySQL 5.7 及更早的默认插件。算法较老(基于旧版 SHA1),但极老的客户端和驱动只认它。

坑就出在兼容性上:很多老客户端、老编程语言的数据库驱动,只支持 mysql_native_password,不支持 caching_sha2_password。于是你用 MySQL 8.0 起库、用默认插件建了用户后,老客户端连接时报类似"Authentication plugin 'caching_sha2_password' cannot be loaded"或"client does not support authentication protocol requested by server"的错。解决思路有两条:

思路一(推荐,长期正确):升级你的客户端 / 数据库驱动到支持 caching_sha2_password 的新版本,保留安全强度更高的默认插件——这是治本。

思路二(临时兼容):在确需兼容老客户端时,把某个用户的认证方式改回 mysql_native_password:

-- 8.0 下临时把 whb 的认证插件改回 mysql_native_password,兼容老客户端
alter user 'whb'@'localhost' identified with mysql_native_password by '强密码';

这里的 identified with 插件名 就是"我明确告诉你用哪个认证插件来算这个密码"。改完,老驱动就能连上了。

想查看当前用户用的哪种插件,可以查:

-- 查看用户及其认证插件
select user, host, plugin from mysql.user;

给生产环境的一句忠告:为了兼容老客户端而降级认证插件,是在降低安全底线(mysql_native_password 的算法已经偏老)。能升级客户端就升级客户端,把降级当临时救火手段,别当默认配置。

备份与恢复:给数据上一份保险

用户和权限都理顺了,最后我们聊聊"万一出事怎么办"——数据库备份。备份是把数据"复制一份留存",以便数据损坏、误删、被攻击时能还原。它和用户管理看似无关,却是每个 DBA 的必修课,也是这课后半程落地的关键。

MySQL 最经典的逻辑备份工具是 mysqldump。它是在操作系统终端(bash)里运行的命令行工具,不是 SQL。基本用法:

# 备份单个数据库到当前目录下的备份文件
mysqldump -u 用户名 -p 数据库名 > 备份文件.sql

看一个具体例子:

# 用 root 备份 brand_db 库,导出到 brand_db_backup.sql
mysqldump -u root -p brand_db > brand_db_backup.sql

执行时会提示输入密码,配 -p 表示会提示你输入。生成的 .sql 文件里是一堆 CREATE TABLE、INSERT INTO 等 SQL 语句和建表信息——备份文件本身就是"一份可回放的数据库重建脚本"。

如果要备份整台服务器上所有的库:

# 备份所有数据库到 all_databases.sql
mysqldump -u root -p --all-databases > all_databases.sql

--all-databases(也可以缩写 -A)会连带把 mysql 系统库、用户、权限一起导出去,常用于整库搬家和灾备。

恢复数据则反过来,把备份文件"喂"回 MySQL。两段式:

# 恢复一个库之前,先在库里建好目标数据库(如果还不存在)
mysql -u root -p -e "create database if not exists brand_db;"
# 把备份文件里的内容回放到 brand_db 库里
mysql -u root -p brand_db < brand_db_backup.sql

这里 < 表示"把文件内容作为输入喂给这个 mysql 客户端",逐条执行里面的 SQL,就实现了数据还原。恢复新手最容易犯的错是目标库还没建、或者文件名搞错路径,报 "Unknown database"——所以先确保目标库存在。

备份的几个运维心法:

  • 按需分库备份:只想备份某个关键库(比如订单库),就备份那一个,快且文件小;
  • 定期 + 异地:备份要定时执行(可配合脚本/计划任务),并且别把备份文件存在同一台服务器上,否则机器挂了备份也一起没了;
  • 区分逻辑/物理/二进制日志:mysqldump 是逻辑备份(导出 SQL);还有物理备份(直接拷数据文件)和基于二进制日志(binlog)的增量备份,能恢复到更精确的时间点。入门先掌握 mysqldump 这套全量逻辑备份,够打响第一枪。

一句话给备份做个总结:不会备份的 DBA 是拿数据裸奔;会备份、会恢复,才算真正给数据上了保险。

常见坑位速查表

到这里,这一课最容易踩的坑基本都冒出来了。我把它们集中成一张表,方便你日后快速回看:

坑位现象正确做法
host 写得太宽裕把 % 到处用,账号暴露面过大尽量用具体 IP / 网段
% 不等于 localhost'u'@'%' 存在,本机却连不上本机另建 'u'@'localhost'
删用户漏写 hostdrop user whb 报 ERROR 1396写全 drop user 'whb'@'localhost'
手改 user 表权限改了却不生效用 GRANT/REVOKE;非改表不可就 flush
以为 GRANT 后必须 flush代码里白白补一句GRANT/REVOKE 会自动刷,不需要 flush
密码策略卡住报 ERROR 1819提强密码,或查/改 validate_password 策略
密码用旧版 password()8.0 直接报错用 set password = 'xxx' 或 alter user
迁 8.0 连不上老客户端报认证插件错升驱动;或 identified with mysql_native_password
中文乱码插入中文读出 ?set names utf8mb4,建库建表用 utf8mb4
远程连不上明明授权了还是连不上检查 host、bind-address、防火墙、443 端口

表格这东西只是索引,真正的理解在前面正文里。

思考题(附详解)

下面的题,每道我都给了详解。你可以先自行作答,再对照答案。

题 1:在 MySQL 里,'jack'@'localhost'、'jack'@'%' 是同一个用户吗?为什么?

**详解:**不是同一个用户。MySQL 里一个账号必须由"用户名 + host"两者共同唯一确定。host 不一样,就是两个不同的账号,可以各自配完全不同的密码和权限。'jack'@'localhost' 只能在本机登录,'jack'@'%' 可以在任意主机登录。正因为容易混淆,操作时务必写完整 '用户名'@'host'。

题 2:我执行了 drop user whb;,系统提示 ERROR 1396 ... 'whb'@'%',这是为什么?正确写法是什么?

**详解:**当只写裸用户名 whb 时,MySQL 会默认把 host 补成 '%',于是它尝试删除的是 'whb'@'%' 这个账号。如果你的系统里实际建的是 'whb'@'localhost',它就找不到删除目标而报 1396 错。正确写法是写成完整账号:drop user 'whb'@'localhost';。这正好印证了"账号 = 用户名 + host"这一条。

题 3:我用 grant select on test.* to 'whb'@'localhost'; 给 whb 授了 test 库的查询权。随后让他执行 delete from account;,他会不会成功?为什么?

**详解:**不会成功。test.* 只授予了 select(查询)这一个权限,delete 并不在其列。所以执行 delete 会得到类似 ERROR 1142 ... DELETE command denied 的拒绝报错,系统明确告诉他"你没有这个权限"。这正体现了权限的最小化:授权时只给完成工作所必需的权限。

题 4:GRANT 授权之后,一定、必须紧跟着执行 flush privileges; 才能生效吗?何时才真的需要 flush?

详解:不必须。GRANT、REVOKE、CREATE USER、DROP USER、SET PASSWORD、ALTER USER 这些账户管理语句执行后,MySQL 内部会自动把权限重载进内存,立即生效,无需手动 flush。真正需要 flush privileges; 的场景,是你绕开这些专属语句、直接用 insert/update/delete 手工改授权表之后——这时磁盘和内存脱节,flush 才能让改动生效。而手工改表本身并不推荐,所以"GRANT 后必须 flush"其实是个流传很久的误解。

题 5:给 root 用户执行 grant all privileges on *.* to 'root'@'%' with grant option;,这里 *.* 和 with grant option 分别是什么意思?这条命令风险大吗?

详解:*.* 表示"本系统所有数据库的所有对象",即全局权限;with grant option 表示允许该用户再把权限转授(也兼有授权能力)给别人。两者叠加,等于把一个拥有全部权限、且能继续向下授权的人造出来——这是典型的超级权限账号。root 本就是这个超级权限的默认账号则由系统初始生成,这条语句更多是"显式重建/确认其全局权限"。对业务账号而言,*.* + with grant option 是高风险配置,绝不轻易授予;给业务用户应把范围收窄到它要用的库,并避免 with grant option。

题 6:MySQL 的 utf8 和 utf8mb4 有什么关系?为什么新项目都推荐用后者?

**详解:**MySQL 里的 utf8 实际上是只能表示 3 字节编码的 utf8mb3,并不能完整覆盖 Unicode;utf8mb4(mb4 = max bytes 4)才是能表示最多 4 字节、覆盖 emoji 等特殊字符的完整 UTF-8。如果库表用 utf8,遇到 emoji 或特殊汉字就可能存不进、读出 ? 甚至报错。所以新库新表请一律 default character set utf8mb4,从源头避免乱码。出现中文乱码时,也可以先用 set names utf8mb4; 应急观察。

题 7:我用 MySQL 8.0 建了用户,老客户端连不上,报"认证插件 caching_sha2_password 不支持"之类的错。为什么?怎么解决?

**详解:**这是认证插件兼容性问题。MySQL 8.0 默认使用更安全的 caching_sha2_password 插件,而较老版本的客户端和部分编程语言的数据库驱动只支持旧版的 mysql_native_password,于是两者对不上导致连不上。解决方案有两种:优先升级客户端 / 数据库驱动到支持 caching_sha2_password 的版本(治本、不动安全底线);若确需临时兼容老客户端,可以 alter user 'whb'@'localhost' identified with mysql_native_password by '强密码'; 把该用户改回老插件(治标,但会降级安全,只作临时手段)。

题 8:我想把自己服务器上的 brand_db 库备份出来,再恢复到另一台机器上。写出通用命令。恢复前要注意什么?

详解:

# 备份:把 brand_db 导出成 sql 文件
mysqldump -u root -p brand_db > brand_db_backup.sql
# 恢复(在目标机器):先确保目标库存在
mysql -u root -p -e "create database if not exists brand_db;"
# 再把备份文件回放进库
mysql -u root -p brand_db < brand_db_backup.sql

恢复前要注意:目标机器上目标数据库必须已存在(或先创建),否则报 "Unknown database";确认 mysqldump/mysql 命令的路径、以及备份文件的路径正确;密码要符合新实例的策略要求。生产环境建议定期备份且异地存放,别把备份和库放同一台机器。

小结

这一课我们用一条线把 MySQL 用户管理串了起来:用户信息落在 mysql.user 表,由 user + host 共同唯一确定,host 决定"从哪儿来",authentication_string 记录加密后的密码。我们学会了用 CREATE USER 建号、用 DROP USER 删号(记得带 host!)、用 SET PASSWORD / ALTER USER 改密,也躲过了 % 不匹配 localhost、旧版 password() 函数被移除这些暗礁。然后进入了权限的深水区:明白了 *.* 和 库.* 的范围之分,学会用 GRANT 给权限、用 REVOKE 收权限、用 SHOW GRANTS 查权限,也搞清了 FLUSH PRIVILEGES 到底什么时候才派得上用场。最后,我们从权限跳到两个实战话题——用 utf8mb4 把中文乱码扼杀在源头、用 mysqldump 给数据上一份能救命的保险。

回头看开头的那个"全员共用一个 root"的画面,你应该已经能嗅到它的不安全:那是"身份不识别、权限不细分、风险不可控"的极致。而配合 host 收窄、最小权限原则、远程访问把关、认证插件选型、utf8mb4 字符集和定期备份,一个能抵御大部分低级事故的 MySQL 运维框架就立起来了。

MySQL 用户管理的内容到这里,就算告一段落。但别忘了:这一课讲的"权限"和"备份",都是建立在你会用 SQL 操作数据之上的;而用户与权限的实际排查,往往还要结合 show variables、show grants、netstat、mysqldump 这些命令互相验证。把这篇文章里的命令亲手敲一遍、把报错亲手踩一遍,你对这套体系的记忆才会真的长在身上。下一段征程,我们往更深的方向走。