结论先行
你的 40s 阻塞大概率不是 innodb_stats_auto_recalc 自动重采样造成的,而是你那条 01:00 手动 cron 的 ANALYZE TABLE 撞上了 02:00 批跑 DML。 原因:自动重采样走 InnoDB 内部 dict_stats_update,不额外拿 MDL;手动 ANALYZE TABLE 走 SQL 层 mysql_admin_table,拿 MDL_SHARED_READ。你看到的 LOCK_TYPE=EXCLUSIVE 需要重新确认——8.0 的 ANALYZE TABLE 正常拿的是 SRL,不是 EXCLUSIVE(EXCLUSIVE 通常是 DDL/ALTER 拿的)。
一、确认锁级别与触发路径(回答你的 3 个问题)
Q1:ANALYZE TABLE 拿 MDL 排他锁还是共享锁?
8.0 源码路径:sql/admin_proc.cc → mysql_admin_table() → 对 ANALYZE 走 MDL_SHARED_READ(SRL)。SRL 会阻塞 MDL_EXCLUSIVE(DDL),但不会阻塞并发的 DML(DML 拿的是 MDL_SHARED_WRITE,SRL 和 SW 可以共存)。
但你抓到的 LOCK_TYPE=EXCLUSIVE 和 ANALYZE 的正常行为不一致。 建议重新抓一次:
SELECT m.LOCK_TYPE, m.LOCK_NAME, m.OBJECT_TYPE, m.OBJECT_SCHEMA,
m.OBJECT_NAME, m.LOCK_DURATION,
t.THREAD_ID, t.PROCESSLIST_ID, t.PROCESSLIST_COMMAND,
t.PROCESSLIST_INFO
FROM performance_schema.metadata_locks m
JOIN performance_schema.threads t ON t.THREAD_ID = m.OWNER_THREAD_ID
WHERE m.OBJECT_NAME = 't_settle_record'
AND m.LOCK_TYPE != 'SHARED_READ'; -- 过滤掉正常的 SRL
重点看 PROCESSLIST_INFO 字段——如果那条 EXCLUSIVE 锁的 SQL 不是 ANALYZE TABLE,而是 ALTER TABLE / OPTIMIZE TABLE / 某个触发器里的 DDL,那阻塞源就不是 ANALYZE。
Q2:为什么只阻塞 t_settle_record?
MDL 是表粒度的,8.0 没有按索引粒度的 MDL。你的理解正确。
Q3:自动重采样 vs 手动 ANALYZE 锁级别是否一致?
不一致,这是关键区别:
| 路径 | MDL | 说明 |
|------|-----|------|
| 手动 ANALYZE TABLE | 拿 MDL_SHARED_READ(SRL) | 走 SQL 层 mysql_admin_table,会阻塞 DDL,但理论上不阻塞 DML |
| innodb_stats_auto_recalc 自动触发 | 不额外拿 MDL | 在 DML 事务内部调用 dict_stats_update,用 InnoDB dict 层 mutex 保护,那个 DML 事务本身已经持有了 MDL_SHARED_WRITE |
所以:自动重采样本身不会造成你看到的 40s DML 阻塞。你的阻塞源是手动 cron。
补充:SRL 和 DML 的 SW 可以共存,但 SRL 会短暂阻塞新事务的 MDL 获取(因为 MDL 的 grant 顺序是 EXCLUSIVE > SW > SRL,SRL 在队列中会挡住后面排队的 EXCLUSIVE)。如果 02:1x 恰好有 DDL 在排队等 EXCLUSIVE,SRL 的 ANALYZE 会间接放大 DML 的等待——但这种情况比较少见。
二、方案(按代价从低到高)
方案 A:删掉手动 cron,保留 auto_recalc(推荐,零改造)
# 删掉 01:00 的 cron
crontab -e
# 删除:0 1 * * * mysql -e "ANALYZE TABLE db.t_settle_record"
保留 SET GLOBAL innodb_stats_auto_recalc = ON。
为什么够用:
- 自动重采样阈值是
n_diff_pfx* > n_rows * 0.1(10%),2.1 亿行要 2100 万行变更才触发,日常批跑(单次 200-500 行)远远不够; - 触发时走
dict_stats_update,不拿 MDL,不阻塞 DML; - 你之前「关掉 auto_recalc 后计划漂移回来」说明 auto_recalc 本身是有效的,问题不在它。
风险: 如果某天出现大批量 DELETE/INSERT(比如对账批跑一次性改 500 万行),auto_recalc 会触发,但走的是 InnoDB 内部路径,不阻塞 DML,只是那个 DML 事务的耗时会增加(因为 dict_stats_update 在事务内部做采样)。
方案 B:手动 ANALYZE 加 fail-fast(适合必须手动刷新的场景)
-- 在 cron 脚本里加 lock_wait_timeout,让 ANALYZE 3s 拿不到 MDL 就放弃
SET SESSION lock_wait_timeout = 3;
ANALYZE TABLE db.t_settle_record;
效果: 如果 01:00 有 DML 在跑,ANALYZE 3s 后报 ERROR 1205: Lock wait timeout,DML 不被阻塞,ANALYZE 失败后由 cron 重试(加个 retry: 6, interval: 10s 的循环)。
#!/bin/bash
# analyze_with_retry.sh
for i in $(seq 1 6); do
mysql -e "SET SESSION lock_wait_timeout=3; ANALYZE TABLE db.t_settle_record;" 2>/dev/null
if [ $? -eq 0 ]; then
echo "$(date) ANALYZE OK" >> /var/log/analyze.log
exit 0
fi
echo "$(date) ANALYZE failed (attempt $i), retrying..." >> /var/log/analyze.log
sleep 10
done
echo "$(date) ANALYZE failed after 6 attempts" >> /var/log/analyze.log
exit 1
8.0 里 lock_wait_timeout 同时控制 MDL 和行锁的等待超时(之前 innodb_lock_wait_timeout 只管行锁),所以这一招对 MDL 有效。
方案 C:加分区,分区级 ANALYZE(最彻底,代价最大)
-- 按 created_at 按月分区(示例)
ALTER TABLE t_settle_record
PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p2024_01 VALUES LESS THAN (TO_DAYS('2024-02-01')),
PARTITION p2024_02 VALUES LESS THAN (TO_DAYS('2024-03-01')),
...
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- 只分析某个分区,阻塞窗口从 40s 缩到 2-5s
ANALYZE TABLE t_settle_record PARTITION (p2024_10);
代价: 表结构改造 + 所有查询必须带分区键 created_at(否则全分区扫描)+ 分区管理(每月加新分区、定期 DROP 旧分区)。适合数据有明确时间维度的场景(结算记录通常有)。
关于 8.1 的 ANALYZE TABLE ... PERSISTENT
不解决 MDL 阻塞问题。 PERSISTENT 子句的作用是把统计信息写入 data dictionary(避免重启后丢失),采样过程还是拿 MDL_SHARED_READ。你 8.0.36 上 innodb_stats_persistent 默认已经是 ON,统计信息已经在 data dictionary 里了,不需要 8.1 的 PERSISTENT。
三、兜底:把阻塞窗口缩到 1s 以内
8.0 没有「只刷新某个索引的 cardinality」的语法,ANALYZE TABLE 是全表粒度的。
最实际的组合拳:
- 删掉 01:00 手动 cron(方案 A);
- 保留 auto_recalc = ON(它不阻塞 DML);
- 如果白天计划漂移严重(auto_recalc 阈值 10% 没触发),在真正无 DML 的窗口(比如 00:30-01:00 业务低谷)跑一次手动 ANALYZE,加
lock_wait_timeout = 3兜底; - 监控:把
information_schema.innodb_table_stats的n_diff_pfx1/2/3和n_rows做成日报,看漂移趋势:
drift_pct 接近 10% 时说明快触发 auto_recalc 了,提前在低峰窗口手动 ANALYZE 一次。SELECT table_name, n_rows, n_diff_pfx1, n_diff_pfx2, n_diff_pfx3, ROUND(n_diff_pfx1 / GREATEST(n_rows,1) * 100, 2) AS drift_pct FROM information_schema.innodb_table_stats WHERE table_name = 't_settle_record';
四、一句话总结
你的阻塞源是手动 cron 的 ANALYZE TABLE(拿 MDL_SHARED_READ),不是 auto_recalc(不拿 MDL)。删掉 cron、保留 auto_recalc、手动刷新时加 lock_wait_timeout=3 fail-fast,问题就解了。 你抓到的 LOCK_TYPE=EXCLUSIVE 建议重新确认 OWNER_THREAD_ID 对应的 SQL,排除 DDL 干扰。