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;