写这个系列的时候,我给自己的定位就很明确:第一篇把 MySQL 装起来,第二篇把 SQL 写顺,到了这第三篇,就该解决你真正打开项目之后绕不过去的那些坑了。后台和评论区问得最多的,反而不是语法,而是"为什么我本地好好的,一到服务器就连不上"、"为什么一条 update 卡了半天"、"为什么同样的 SQL 在别人电脑上跑得飞快"这类问题。所以这一篇我不打算按教科书顺序讲,而是把部署、字符集、事务、锁、存储过程、连接驱动、主从复制和典型报错串在一起,用实际项目里会遇到的方式重新过一遍。
如果你是一个刚把 MySQL 装好、正准备往项目里集成的人,或者已经写了几个月 SQL、但一直没搞懂锁和事务隔离级别的人,这篇应该能帮上忙。我不会堆概念,所有内容尽量给到可以直接复制的命令、参数和排查思路。
1. 部署选型:从本机安装到 Docker 部署,到底该怎么选
1.1 本机安装的隐藏坑
我见过太多人卡在安装这一步。MySQL 8.0 的安装包本身没问题,但 Windows 下最容易翻车的是两件事:一是安装到一半报 Configuration of MySQL server is taking,二是服务启动时报错,根本原因通常是端口被占用或者 data 目录权限不对。
先说端口。MySQL 默认 3306,如果你机器上已经装了老版本 MySQL、MariaDB,或者某个开发工具自带了 MySQL 服务,3306 早就被占了。安装配置那一步会让你选端口,别一路 Next,先确认一下:
bash复制netstat -ano | findstr :3306
有输出就说明端口被占用,要么换端口,要么先把占用进程处理掉。Linux 下同理,用 ss -lntp | grep 3306 看。
另一个容易被忽略的是 data 目录。Windows 安装版会自动处理,但如果你用的是免安装的 zip 包,手动初始化时经常忽略目录权限和属主问题。Linux 下用 tar 包安装,mysqld --initialize-insecure 之后,必须确认 /var/lib/mysql 的属主是 mysql 用户,否则服务启动直接报错。
注意:生产环境不要用
--initialize-insecure生成空密码 root 账户,虽然本地测试方便,但暴露到网络环境就相当于裸奔。至少要用--initialize生成临时密码,或者装完立刻改密码。
1.2 Docker 部署的推荐姿势
本机装完能跑,并不代表你可以把生产环境也这么搞。我现在的项目里,开发环境统一用 Docker 跑 MySQL,镜像固定版本,配置统一管理,换机器五分钟解决。热搜词里也一直有人在问 Docker 安装 MySQL,我直接给一份我在用的命令:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=你的密码 \
-e TZ=Asia/Shanghai \
-v /opt/mysql8/data:/var/lib/mysql \
-v /opt/mysql8/conf:/etc/mysql/conf.d \
--restart=always \
mysql:8.0
几个参数说明一下:
-e TZ=Asia/Shanghai必须加,否则容器默认 UTC 时区,NOW()出来的时间比北京时间差 8 小时,查数据的时候会怀疑人生。-v /opt/mysql8/data:/var/lib/mysql是数据持久化。不挂载的话,容器一删数据全没,删容器前记得确认。/etc/mysql/conf.d挂载配置目录,后面想调max_connections、slow_query_log这些参数,直接改宿主机文件再重启容器就行,不用进容器折腾。- MySQL 8.0 的官方镜像默认字符集是
utf8mb4,但排序规则不一定是你想要的,后面单独说。
如果你在 Windows 上用 Docker Desktop,挂载路径写成 D:/mysql8/data:/var/lib/mysql 这种格式,注意盘符写法。
1.3 装完之后第一件事:确认字符集和排序规则
装完 MySQL 先别急着建表,直接跑这两条:
sql复制SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
MySQL 8.0 默认 utf8mb4 + utf8mb4_0900_ai_ci,这本身没问题。问题在于老的系统迁移过来,或者从 5.7 导出的数据,可能还是 latin1 或者 utf8mb4_general_ci。字符集不对,最典型的表现就是中文乱码、表情符号存不进去、字符串比较结果和预期不符。
utf8mb4 是必须的,它能存四字节的 emoji 和生僻字,utf8(也就是 utf8mb3)存不下。排序规则的选择上,utf8mb4_0900_ai_ci 是 8.0 新增的更优排序,utf8mb4_general_ci 是兼容老项目的选择,utf8mb4_unicode_ci 更准确但性能略低。跑实际项目我建议统一 utf8mb4_0900_ai_ci,除非你有老数据要对比。
还有一个容易踩的坑:连接字符串里的字符集。有时候数据库是 utf8mb4,但应用连接参数里没指定 characterEncoding=utf8,Java 驱动默认按 utf8 处理,中文是能存,但某些特殊字符还是可能出问题。这个到第 3 节讲连接池的时候再展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频 SQL 操作:update、排序、常用函数背后的注意点
2.1 update 语法:为什么人人都说容易出事
UPDATE 大概是所有写坏生产数据的操作里排名第一的。不是语法难,而是很多新手对"没有 WHERE 条件就是全表更新"这件事没有肌肉记忆。
sql复制UPDATE student SET score = score + 5 WHERE id = 1;
这条没问题,只更新一行。但如果 WHERE 条件写错,比如:
sql复制UPDATE student SET score = score + 5;
那就是全表加 5 分。更隐蔽的是 WHERE 条件查询出来的结果比预期多,比如:
sql复制UPDATE student SET score = score + 5 WHERE class_id = 10;
如果 class_id 没有索引,MySQL 会扫描全表,逐行匹配,数据量大时直接锁住大量行,其他请求全堵住。
我的习惯是,在测试环境执行 UPDATE 之前,先把 WHERE 改成 SELECT 跑一遍,确认影响行数:
sql复制SELECT COUNT(*) FROM student WHERE class_id = 10;
确认无误再执行 UPDATE。生产环境更严格一点,修改敏感数据前先 START TRANSACTION,执行完检查 ROW_COUNT(),确认没问题再 COMMIT,有问题直接 ROLLBACK。手动操作数据不怕慢,怕的是没有后悔药。
另外提醒一点,MySQL 的 UPDATE 在客户端执行时,如果 SQL 里有语法错误,会直接报错,不会部分执行。但在存储过程中,如果你没有定义异常处理,一条 SQL 出错可能导致整个事务回滚不彻底,这个到第 3 节再讲。
2.2 ORDER BY 排序:别忽略字符集和索引的影响
排序是面试和实际开发都很爱问的,但大多数人的理解停留在"ORDER BY 就是排序"。排序本身简单,真正考人的是:能不能用到索引?排序规则是什么?
先看能不能用索引。如果 ORDER BY 的字段有合适的索引,MySQL 可以直接按索引顺序读取,不需要额外的 filesort,性能高很多。但如果查询里同时有 WHERE 和 ORDER BY,联合索引要满足"最左前缀"原则,否则照样 filesort。一张大表几百万数据,filesort 可能直接让查询慢一个数量级。
看执行计划:
sql复制EXPLAIN SELECT * FROM student WHERE class_id = 10 ORDER BY score DESC;
如果 Extra 列出现 Using filesort,说明排序没有完全走索引,就要考虑优化。
再一个容易被忽略的是排序规则对结果的影响。默认的 utf8mb4_0900_ai_ci 是大小写不敏感、重音不敏感的排序,所以 ORDER BY name 时,'apple' 和 'Apple' 的先后顺序可能和你预期不同。如果你需要区分大小写排序,要么把字段的 collation 改成 _bin 后缀的,要么在查询里用 ORDER BY BINARY name。
中文排序也是个坑。MySQL 默认按 Unicode 编码排序,不是按拼音,更不是按笔画。如果你需要中文按拼音排,最简单的方式是在应用层处理,或者给字段单独指定 utf8mb4_zh_0900_as_cs 这类中文排序规则,但性能和兼容性要测试过再用。大多数项目其实不需要数据库层做中文排序,别硬上。
2.3 常用函数:IFNULL、NOW、DATE_FORMAT、GROUP_CONCAT
热搜词里有 mysql常用函数,我挑几个实际项目里出镜率最高、且容易用错的。
IFNULL(expr1, expr2) 是处理 NULL 最常用的,但有个经典坑:IFNULL 不会改变字段类型推断,如果第一个参数是字符串,第二个参数是数字,返回类型可能出问题。另一个更隐蔽的是 COUNT(字段) 和 COUNT(*) 的区别,COUNT(字段) 会跳过 NULL 值,COUNT(*) 不会。统计行数时用 COUNT(*),统计非空值数量才用 COUNT(字段)。
NOW() 和 SYSDATE() 看似一样,其实有区别:NOW() 是语句开始执行的时间,SYSDATE() 是函数真正执行到的那一刻。在长事务、多语句的上下文中,这两个时间可能不一致,如果用于流水号和日志记录,要特别注意。
DATE_FORMAT 做格式化展示很好用,但千万别在 WHERE 条件里对索引字段用函数,比如:
sql复制WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-01'
这样会导致索引失效,全表扫描。正确写法是范围查询:
sql复制WHERE create_time >= '2024-01-01 00:00:00'
AND create_time < '2024-01-02 00:00:00'
GROUP_CONCAT 可以把分组内的多行值拼成一个字符串,经常用于一对多关系的聚合展示。但它有默认长度限制,group_concat_max_len 默认 1024,拼接结果超过会被截断,要调大就在会话里执行 SET SESSION group_concat_max_len = 10240;。这个不起眼,但排查问题时会很诡异。
3. 事务、锁和隔离级别:面试常问,实践中更要命
3.1 隔离级别到底选哪个
MySQL InnoDB 默认隔离级别是 REPEATABLE READ,这也是它和很多其他数据库不一样的地方(很多数据库默认 READ COMMITTED)。面试题喜欢问这个,但实际开发更关心的是:这个默认值在某些场景下会产生什么坑。
REPEATABLE READ 配合 InnoDB 的 MVCC,可以让同一个事务里的多次读取结果一致。但副作用是,如果两个事务同时操作同一批数据,可能出现"当前读"和"快照读"不一致的情况。比如:
code复制事务A:SELECT * FROM account WHERE id = 1; -- 读到 balance = 100
事务B:UPDATE account SET balance = 50 WHERE id = 1; COMMIT;
事务A:SELECT * FROM account WHERE id = 1; -- 仍然读到 100(快照读)
事务A:UPDATE account SET balance = balance - 10 WHERE id = 1; -- 此时基于当前读
这里最后一条 UPDATE 是当前读,读到的是事务 B 提交后的 50,减 10 变成 40。但事务 A 前面 SELECT 看到的还是 100,如果业务逻辑是基于 SELECT 的结果判断余额够不够,就会出现"以为够减,实际超减"的并发问题。
实践建议是:对于金额、库存这类关键数据,不要简单地"先 SELECT 判断再 UPDATE",而是直接让 UPDATE 语句带条件,或者用 SELECT ... FOR UPDATE 加锁。这也是很多项目从 REPEATABLE READ 切到 READ COMMITTED 的原因,并发场景下 REPEATABLE READ 的 gap lock 更容易引发死锁和锁等待。
3.2 锁表问题:定位方法和预防手段
热搜词里 mysql锁表 、mysql锁表 出现频率不低,实际项目里基本都遇到过。锁表的本质是事务没提交,持有锁的时间过长,其他事务只能等。
最经典的现象:一条 UPDATE 更新了某一行,事务不 COMMIT,也不 ROLLBACK,就一直挂在那儿,其他事务更新同一行就卡住,直到 innodb_lock_wait_timeout(默认 50 秒)超时,报 Lock wait timeout exceeded。
定位锁问题,我一般按这个顺序来:
sql复制-- 查看当前有哪些事务在跑
SELECT * FROM information_schema.INNODB_TRX\G
-- 查看锁等待关系
SELECT * FROM sys.innodb_lock_waits\G
-- 直接看正在执行的语句
SHOW PROCESSLIST;
INNODB_TRX 里能看到事务状态、开始时间、执行的具体 SQL。trx_state 是 LOCK WAIT 就说明在等锁,trx_mysql_thread_id 对应 PROCESSLIST 里的 Id,可以 KILL 掉卡住的事务。
预防锁表比事后处理更重要,我的经验是三条:
- 事务一定要短,大事务拆小事务。几千行的 UPDATE 尽量分批,每批几百行,提交后再下一批。
- UPDATE、DELETE 的 WHERE 条件尽量走索引。条件不走索引时,InnoDB 可能锁住扫描到的所有行,甚至间隙,影响范围远大于预期。
- 多个事务操作数据时,尽量保持相同的加锁顺序,比如先更新 id 小的,再更新 id 大的,避免死锁。
3.3 存储过程:为什么用、怎么处理错误
存储过程现在用的场景比以前少了,但在批量导入、复杂报表、定时任务里还是经常出现。热搜词里也有一堆人在搜 mysql存储过程、mysql储存过程+错误信息。
先给一个带异常处理的例子:
sql复制DELIMITER $$
CREATE PROCEDURE sp_batch_update_score()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'update failed' AS result;
END;
START TRANSACTION;
UPDATE student SET score = score + 5 WHERE class_id = 10;
UPDATE student SET score = score - 3 WHERE class_id = 11;
COMMIT;
SELECT 'update success' AS result;
END$$
DELIMITER ;
这里最关键的是 DECLARE EXIT HANDLER FOR SQLEXCEPTION,意思是只要事务里任何一条 SQL 报错,就回滚并返回错误信息。没有它,存储过程里第一条 UPDATE 成功、第二条失败,事务不会自动回滚,可能出现数据不一致。
DELIMITER $$ 的用途经常被问。MySQL 客户端默认用 ; 作为语句结束符,但存储过程内部有多条 SQL 都要用 ; 结尾,如果不先把结束符改成 $$,客户端会在第一个 ; 就认为语句结束,导致语法错误。所以 CREATE PROCEDURE 时先 DELIMITER $$,结束再 DELIMITER ;。
DECLARE EXIT HANDLER 还有几个变种,CONTINUE HANDLER 是出错后继续执行,NOT FOUND 专门处理游标查不到数据的场景。记住一个原则:存储过程里的异常处理,宁可多写一层,也不要裸奔。
3.4 触发器里的 DELIMITER 和 NEW/OLD
mysql中触发器中分隔符这个热搜词,说明很多人做触发器时也卡在 DELIMITER 上。道理和存储过程一样,CREATE TRIGGER 也是一段多语句的集合,同样要先用 DELIMITER 改结束符。
一个实际场景:学生成绩更新后,自动写入日志表。
sql复制DELIMITER $$
CREATE TRIGGER trg_score_log
AFTER UPDATE ON student
FOR EACH ROW
BEGIN
INSERT INTO score_log(student_id, old_score, new_score, update_time)
VALUES (NEW.id, OLD.score, NEW.score, NOW());
END$$
DELIMITER ;
触发器里 NEW 表示新值,OLD 表示旧值。AFTER UPDATE 触发器里可以用 OLD 和 NEW,AFTER INSERT 只能用 NEW,AFTER DELETE 只能用 OLD。
触发器的问题在于,它的执行对调用方是透明的,看起来只执行了一条 UPDATE,实际背后还插了一条日志,如果日志表有问题,主表的 UPDATE 也会失败。所以触发器里的逻辑要尽量简单,不要做复杂查询,不要调用存储过程,否则排查问题和性能调优都会很痛苦。
4. 连接访问:JDBC 驱动、连接池和 Workbench/Navicat 的选择
4.1 MySQL 8.0 的驱动坑:Authentication protocol 不支持
热搜词里那条 firedac phys mysql client does not support authentication protocol requested 我太熟悉了,Delphi 的 FireDAC 连接 MySQL 8.0 时必现。原因是 MySQL 8.0 默认认证插件是 caching_sha2_password,老客户端不支持。
这类问题的本质都一样:客户端或者驱动版本太老,不认 MySQL 8.0 的默认认证方式。解决思路有两种:
第一种,升级客户端/驱动到支持 caching_sha2_password 的版本。Java 项目用 mysql-connector-java 8.0.x 及以上,连接驱动类名用 com.mysql.cj.jdbc.Driver,URL 里带上 serverTimezone=Asia/Shanghai;老项目还在用 com.mysql.jdbc.Driver 的,换成新驱动后注意类名也要改。
第二种,兼容老客户端,把用户认证插件改回 mysql_native_password:
sql复制ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;
这是过渡方案,能用,但不推荐长期用。mysql_native_password 是旧插件,安全性不如 caching_sha2_password。
4.2 JDBC URL 参数和连接池参数
Java 项目连 MySQL 8.0,我推荐的最小配置是:
text复制jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
几个参数解释下:
characterEncoding=utf8:和数据库的utf8mb4配合,避免中文乱码。useSSL=false:本地开发不需要 SSL,减少握手开销。生产环境如果要求加密连接,再按需开启。serverTimezone=Asia/Shanghai:MySQL 8.0 驱动对时区敏感,不指定可能报The server time zone value错误。allowPublicKeyRetrieval=true:使用caching_sha2_password认证时,如果客户端无法直接拿到服务端公钥,需要这个参数,否则会报Public Key Retrieval is not allowed。
连接池方面,现在主流是 HikariCP,配置参数里最容易被忽略的是这几个:
yaml复制maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
max-lifetime 必须小于数据库的 wait_timeout,否则连接池里的连接可能被数据库服务端断开,应用还在用,拿到一个坏连接报错。MySQL 默认 wait_timeout 是 8 小时,HikariCP 的默认 max-lifetime 是 30 分钟,差得远,没问题;但如果你改过数据库的 wait_timeout,连接池参数也要跟着调。
4.3 Workbench、Navicat、命令行怎么选
这套工具不用纠结哪个最好。MySQL Workbench 是官方免费工具,适合直连开发环境、跑查询、看执行计划,功能全面但界面稍重。Navicat 是商业软件,功能集成度高、跨数据库迁移方便,很多人用习惯了就离不开了。命令行适合快速操作和脚本化,我日常的运维操作反而是命令行居多。
如果你用 Workbench 连 MySQL 8.0 遇到 Authentication plugin 'caching_sha2_password' cannot be loaded,先升级 Workbench 到 8.0 以上版本,基本能解决,不用急着改认证插件。
5. 进阶场景:主从复制、数据同步和 ETl 工具
5.1 Windows 下主从搭建的简化流程
热搜词里有 windows mysql主从搭建教程,主从复制这个主题确实值得单独写一篇,这里给一个最小可用的步骤,能解决大部分测试环境需求。
主库配置文件 my.ini 添加:
ini复制[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW
从库配置文件 my.ini 添加:
ini复制[mysqld]
server-id=2
然后重启主库和从库,登录主库创建复制账号:
sql复制CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
查看主库状态:
sql复制SHOW MASTER STATUS;
记下 File 和 Position,然后在从库执行:
sql复制CHANGE MASTER TO
MASTER_HOST='主库IP',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='repl_password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=154;
START SLAVE;
SHOW SLAVE STATUS\G
看 Slave_IO_Running 和 Slave_SQL_Running 是否都是 Yes,如果是,基本就成了。
这里强调一个细节:MASTER_LOG_FILE 和 MASTER_LOG_POS 必须和 SHOW MASTER STATUS 的结果完全一致,不能凭感觉填。而且从库在 CHANGE MASTER 前不能有和主库冲突的数据,否则同步会中途报错。
5.2 数据同步工具:DataX 和 Kettle 的使用场景
大数据量迁移、异构数据库同步,经常用到 DataX 或 Kettle。热搜词里 datax同步 mysql 可配置参数 和 kettle支持taos数据库迁移到mysql 都是这个方向。
DataX 是阿里开源的离线同步工具,用 JSON 配置 job,Reader 从源库读数据,Writer 写到目标库。MySQL 到 MySQL 的最小配置长这样:
json复制{
"job": {
"content": [
{
"reader": {
"name": "mysqlreader",
"parameter": {
"username": "root",
"password": "你的密码",
"column": ["id", "name", "score"],
"connection": [
{
"jdbcUrl": ["jdbc:mysql://127.0.0.1:3306/source_db"],
"table": ["student"]
}
]
}
},
"writer": {
"name": "mysqlwriter",
"parameter": {
"username": "root",
"password": "你的密码",
"column": ["id", "name", "score"],
"connection": [
{
"jdbcUrl": "jdbc:mysql://127.0.0.1:3306/target_db",
"table": ["student"]
}
]
}
}
}
],
"setting": {
"speed": {
"channel": 4
}
}
}
}
setting.speed.channel 控制并发通道数,值越大同步越快,但对源库和目标库的压力也越大。全量同步数据量上千万的时候,建议先小批量测试,再逐步加大 channel,不然很容易把生产库打崩。
Kettle 更偏图形化,适合业务人员或者不习惯写 JSON 的团队,功能和 DataX 类似,但同步超大表的性能不如 DataX 稳定。工具选择上我的建议是:一次性迁移用 DataX,日常 ETL 排任务用 Kettle,具体看团队熟悉度。
5.3 Sqoop 连接不上 MySQL 的排查
大数据生态里 Sqoop 连接 MySQL 也是个高频问题。sqoop连接不上mysql 的报错一般是 Communications link failure 或者 Access denied,排查顺序:
- 驱动 jar 有没有放到 Sqoop 的 lib 目录。MySQL 8.0 需要
mysql-connector-java8.0.x,老版本驱动连 8.0 会报认证问题。 - JDBC URL 写法对不对。Sqoop 里直接写
jdbc:mysql://IP:3306/db,不需要serverTimezone,但 IP 要能被大数据节点访问到,注意防火墙。 - 数据库用户权限。Sqoop 连库拉数据,用户至少要拥有
SELECT权限,而且授权时host要包含 Sqoop 所在机器的 IP,比如'user'@'%'或'user'@'hadoop-node'。
这些和普通应用连 MySQL 的排查逻辑一样,先确认网络通不通:
bash复制telnet IP 3306
不通就先解决防火墙和网络,别急着查驱动和权限。
6. 高频报错排查实录:从 Authentication 到主从失败
6.1 Windows 安装启动服务报错
Windows 安装 MySQL 后服务启动失败,最常见的是 data 目录权限不对、端口被占用、my.ini 配置错误。排查先看 Windows 事件查看器里的 MySQL 日志,但更直接的方式是手动执行:
bash复制mysqld --console
看控制台输出,错误信息会直接打出来。如果提示 [ERROR] Can't start server: Bind on TCP/IP port: No such file or directory,先确认 3306 被谁占了。
如果安装时卡在 Configuration 那一步,多半是杀毒软件拦截了服务注册或端口监听,临时关掉杀毒再装一次。这个没什么优雅的解决办法。
6.2 Authentication plugin 报错处理速查
把这类报错按场景整理成一张速查表,遇到直接对照处理:
| 报错信息 | 原因 | 解决方向 |
|---|---|---|
Authentication plugin 'caching_sha2_password' cannot be loaded |
客户端/驱动不支持 MySQL 8.0 默认认证插件 | 升级客户端/驱动,或改用户认证插件为 mysql_native_password |
Public Key Retrieval is not allowed |
JDBC 连接 caching_sha2_password 用户时未允许公钥获取 | URL 加 allowPublicKeyRetrieval=true |
Access denied for user 'root'@'localhost' |
密码错误或用户 host 限制 | 确认密码,确认用户允许从当前主机登录 |
The server time zone value 确认 |
JDBC 时区未指定 | URL 加 serverTimezone=Asia/Shanghai |
这里有个小技巧:如果你不确定当前用户到底用的什么认证插件,可以执行:
sql复制SELECT user, host, plugin FROM mysql.user;
看输出你就知道问题在哪了。
6.3 唯一索引重复数据问题怎么处理
热搜词里 mysql设置唯一已经有重复数据库,说明不少人在给已有数据的表加唯一索引时,被"Duplicate entry"卡住了。原因很简单:表里已经存在违反唯一性的数据,MySQL 不允许你加上这个索引。
处理思路不是直接删数据,而是先查出来:
sql复制SELECT id, student_no, COUNT(*)
FROM student
GROUP BY student_no
HAVING COUNT(*) > 1;
确认重复数据后,再决定保留哪一条。常见保留策略是保留 id 最小的一条,删除其他重复记录:
sql复制DELETE s1 FROM student s1
INNER JOIN student s2
ON s1.student_no = s2.student_no
WHERE s1.id > s2.id;
注意执行前先备份。实际生产环境清理重复数据,我建议先导出到临时表确认,再执行删除,别直接在生产库上跑这种 SQL,除非表很小。
6.4 锁等待超时和主从同步中断
Lock wait timeout exceeded 的定位方法在第 2 节讲过,这里再补充一个实践技巧:如果频繁出现锁等待,且 SHOW PROCESSLIST 里看不到明显的长事务,检查是不是有多个连接在跑定时任务,比如每个整点都有脚本批量更新同一张表。解决办法是给这些任务加分布式锁,或者在应用层用 SELECT ... FOR UPDATE NOWAIT 让拿不到锁的任务快速失败重试,而不是干等 50 秒。
主从同步中断常见报错是 Slave_SQL_Running: No,绝大多数是因为从库上手动改过数据,或者主库执行了从库不支持的语句。最简单的处理思路是跳过错误:如果确认错误不影响最终数据一致性,可以在从库执行
sql复制STOP SLAVE;
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
START SLAVE;
但这是火线处理手段,跳过的错误如果导致数据丢失,事后要做数据校验和补数。更好的方式是先备份主库数据,重新搭建从库,一劳永逸。
7. 我自己的几个实操习惯
7.1 开发环境一定开慢查询日志
慢查询日志是最容易被忽略的运维手段。很多项目上线后出现间歇性慢接口,排查半天没头绪,结果一看慢查询日志,全是某个没走索引的报表查询。随手在配置文件里加一段:
ini复制slow_query_log=ON
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=2
long_query_time 定义超过多少秒算慢查询,我习惯设 2 秒,测试环境可以设 1 秒,宁可多记录也别漏掉。
然后定时用 mysqldumpslow 分析:
bash复制mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
按时间排序看 TOP 10,基本能覆盖掉大多数性能隐患。
7.2 生产环境改数据前先备份,改完立即验证
手动执行 UPDATE、DELETE 前,我会先 mysqldump 备份相关表:
bash复制mysqldump -u root -p 数据库名 表名 > 备份文件.sql
表特别大就只备份 WHERE 条件涉及的数据,写个临时表导出。改完数据立即验证影响行数、查询结果,确认无误再做后续操作。这个习惯在带团队之后更强烈,因为手动操作数据库出的问题,百分之九十都是因为没备份、没事务、没验证。
7.3 多版本验证要养成看版本的意识
MySQL 5.7 和 8.0 的很多行为不一样,比如默认认证插件、utf8mb4 的默认排序规则、窗口函数的支持、WITH 语法。查资料、抄 SQL 之前先确认对方用的版本,再和你本机的版本对比,能省掉很多想不通的兼容性问题。
sql复制SELECT VERSION();
一条命令解决,别偷懒。
从部署到同步、从报错到优化,MySQL 体系的知识确实很散,但核心脉络一直没变:先搞清楚数据和索引怎么存,再理解事务和锁怎么协同,最后才是各种语法和工具的灵活运用。前两篇帮你入了门,这篇更像是一份我自己会反复翻看的现场笔记。你遇到的新问题,百分之八十都能在报错信息和官方文档里找到答案,剩下的百分之二十,就是这篇文章里这些坑攒下来的经验了。
