Administrator
发布于 2025-07-30 / 0 阅读
0
0

MySQL 优化 + 最佳实践

MySQL:5.7

一、配置优化(my.cnf)

请看另一篇文章 —— MySQL 配置优化

二、事务级别尽量用 RC

一般的业务系统,使用 RC 足矣。几乎没有场景用到 RR 事务内快照的功能(避免幻读、不可重复读),再者 RC 有并发高、死锁概率低、没有间隙锁等优点。

实测 RR -> RC 切换后 CPU负载、内存占用降低 20% 左右、lo 网卡流量从 100M 降到 8、90M,究其原因有几点:

(1)少了间隙锁 -> 少了「锁结构的内存记录」及其对应的 redo 记录

(2)少了大量的 undo 页的 redo 日志(RR 为了维护「事务级快照」,需要把「所有被扫描行」的旧版本保存到 undo 里面)

(3)RR 并发下容易造成大量锁等待超时,应用框架(ORM)默认重试 3-5次,会产生额外流量

RR -> RC:

# 1、查看全局级别
SELECT @@global.tx_isolation;

# 2、编辑 my.cnf / my.ini, 在[mysqld]段增加或者修改一行
transaction-isolation = READ-COMMITTED

三、开启慢日志

定期巡检 MySQL 慢日志,配合告警,及时优化或限流慢 SQL,降低锁等待和连接池耗尽的风险。

打开慢日志(my.cnf):

slow_query_log = 1

# 慢日志时长, 默认是 3s, 根据场景、公司要求调整。
long_query_time = 3

使用 explain + 慢 SQL 查看索引的命中情况,看情况添加索引,索引不生效考虑 「force index(idx_xx)」

四、查询、更新行数的最佳区间

为了 CPU / IO 总量不要太高,通常把「一次表查询/更新的行数」控制在下面两个区间:

1、经验阈值:

  • 读取(R):单批 1000 ~ 10000 行

  • 写入/更新(CUD):单批 500 ~ 5000 行

2、参考指标

(1)更科学的办法是看「执行计划里的 rows × 行长」大致算出数据量,然后让每批落在以下范围:

  • 扫描数据 ≤ 16 MB(≈ InnoDB Buffer Pool 的 1% 左右,避免瞬间刷脏)

  • 返回结果 ≤ 4 MB(MySQL 客户端/服务器 net_buffer_length 默认 1 MB,JDBC/ORM 批量拉取时 4 MB 以内较稳)

  • 执行时间 ≤ 1 s(避免长事务、锁和复制延迟)

(2)快速估算公式

rows ≈ 16 MB / 平均行长

举例:平均行长 400 B ⇒ 16 MB / 400 ≈ 40 000 行,再留 50% 余量 → 每批 20 000 行上限。

(3)落地做法

先 EXPLAIN 看预估行数,再跑一条真实 SQL 看耗时。

若 >1 s 或 Buffer Pool 命中率骤降,就继续折半分批。

用 LIMIT offset, size 或“主键分段”循环;更新场景用“先 SELECT 主键 → 分批 UPDATE”。

(4)结论

先按 1000 ~ 10000 行起步,再根据机器配置、行宽、执行时间微调,而不是死记某个固定值。

五、update、delete 最佳实践

  • 能走主键就走主键(或唯一键),这是 80% 场景下的最佳实践

  • 范围、批量、热点并发 场景下,改用 覆盖索引(+ 分批/乐观锁),效果反而更好。

在多家互联网公司都有强制要求:更新或删除必须带上主键(唯一键),原因有几点:

1、锁粒度最小,天然避免「锁全表」事故。方便审计,binlog 回放不会误伤多行。

2、分库分表友好,路由结果唯一,避免跨分片锁。

个人做法:查询 + 分批 + 主键更新。

遇到「批量更新」、「更新插入」的场景且数据量较大的情况,可参考以下「六、」临时表的做法。

# 批量范围 (走索引、分批: 循环执行,根据 affectedRows < limit 判断执行结束)
UPDATE t … WHERE gmt_modified < ? LIMIT 1000

# 热点并发(主键 + 行级 CAS)
UPDATE t SET cnt = cnt - 1 WHERE pk = ? AND cnt > 0

六、批量 update、upsert 实践

upsert(更新插入)在 MySQL 的语法:INSERT ... ON DUPLICATE KEY UPDATE(简称 IODKU)。IODKU 在并发的场景下容易死锁(特别是 RR),我个人是不喜欢使用的。这种属于 SQL 语义级的问题,在各个数据库都存在。

Mysql 官方与社区共识的最佳实践是 通过 分批 + 顺序 + 重试 的手段兜底。

个人常用的两个方案:

1、常规方案:分批 select + 分批写 + 分批/逐行更新

2、临时表方案:并发高、数据量大的情况下,使用临时表 JOIN 一次完成。

推荐 100条/次。如果有 100w 条,拆成 1w 批次,每批走一次「1-2-3」完成流程。

# 1、先把 100 行装进临时表
CREATE TEMPORARY TABLE tmp_t LIKE t;
INSERT INTO tmp_t(pk, col1, col2)
VALUES (...), (...), ... 100 行;

# 2、需要插入的行: pk 不在正式表
INSERT INTO t(pk, col1, col2)
SELECT pk, col1, col2
FROM tmp_t AS tmp
WHERE NOT EXISTS (SELECT 1 FROM t WHERE t.pk = tmp.pk);

# 3、需要更新的行: pk 已在正式表
UPDATE t
JOIN tmp_t ON tmp_t.pk = t.pk
SET t.col1 = tmp_t.col1, t.col2 = tmp_t.col2;


评论