1. 事故现场还原:批量删除操作引发的连锁反应
那天下午,我正对生产环境的用户行为日志表执行例行清理。这张表已经积累了近2亿条记录,按照 retention policy 需要删除3个月前的数据。我像往常一样执行了看似无害的DELETE语句:
DELETE FROM user_behavior_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY) LIMIT 5000;为降低对线上影响,我特意:
- 添加了LIMIT分批次删除
- 选择业务低峰期执行
- 使用pt-archiver工具控制速率
但十分钟后,监控系统突然告警——数据库连接池耗尽,应用服务开始大面积超时。紧急查看processlist发现大量会话处于"Waiting for table metadata lock"状态,而罪魁祸首正是我的删除会话。
2. 死锁形成机制深度解剖
2.1 MySQL元数据锁的隐藏陷阱
当批量删除操作遇上长事务时,元数据锁(MDL)的获取顺序成为关键。在我的案例中:
- 会话A(删除操作)首先获取MDL读锁
- 会话B(业务查询)尝试获取MDL读锁(成功)
- 会话A需要将MDL读锁升级为写锁(等待会话B释放)
- 会话C(新查询)又请求MDL读锁(排队等待A的写锁)
关键发现:MDL锁是队列化处理的,写锁请求会阻塞后续所有读锁申请
2.2 事务持续时间的影响因子
通过分析被阻塞的会话,发现几个加剧问题的因素:
- 部分报表查询运行时间超过30秒
- 事务隔离级别为REPEATABLE-READ(持有锁时间更长)
- 连接池设置过大(200连接)导致雪崩效应
graph TD A[DELETE请求MDL写锁] -->|被阻塞| B[长查询持有MDL读锁] B --> C[新查询排队] C --> D[连接池耗尽]3. 应急处理与临时解决方案
3.1 打破死锁链的实战步骤
快速诊断:
SHOW PROCESSLIST; SELECT * FROM performance_schema.metadata_locks;精准止血:
- 优先kill执行时间超过10s的查询会话
- 保留至少20%的连接池余量
服务恢复:
# 渐进式重启应用节点 for i in {1..10}; do kubectl rollout restart deployment/app-$i sleep 15 done
3.2 临时调整方案对比
| 方案 | 实施复杂度 | 恢复速度 | 数据风险 |
|---|---|---|---|
| Kill所有删除会话 | 低 | 快 | 中 |
| 降级读流量 | 中 | 中 | 低 |
| 重启数据库 | 高 | 慢 | 高 |
最终选择组合方案:kill 50%的长时间查询 + 临时降级非核心功能
4. 根本解决之道:无锁化设计实践
4.1 分区表+冷热分离架构
-- 改造为按时间分区 ALTER TABLE user_behavior_logs PARTITION BY RANGE (TO_DAYS(created_at)) ( PARTITION p_2023_q1 VALUES LESS THAN (TO_DAYS('2023-04-01')), PARTITION p_2023_q2 VALUES LESS THAN (TO_DAYS('2023-07-01')), PARTITION p_current VALUES LESS THAN MAXVALUE ); -- 删除整分区代替DELETE ALTER TABLE user_behavior_logs DROP PARTITION p_2023_q1;优势对比:
- 传统DELETE:逐行扫描+写undo log
- DROP PARTITION:元数据操作(毫秒级)
4.2 事件驱动+TTL混合模式
新架构设计:
- 原始数据写入Kafka
- Flink实时消费并写入:
- 热数据:MySQL(保留7天)
- 温数据:TiDB(保留30天)
- 冷数据:S3+Parquet(长期存储)
// 使用状态TTL自动清理 StateTtlConfig ttlConfig = StateTtlConfig .newBuilder(Time.days(30)) .cleanupInBackground() .build();5. 关键决策背后的权衡思考
5.1 为什么不选择乐观锁?
虽然乐观锁在某些场景有效,但批量删除存在本质差异:
- 冲突检测成本高(需要比较全表时间戳)
- 重试机制不适用(业务上不允许部分失败)
5.2 连接池参数的黄金比例
经过压测得出的经验值:
# 应用连接池配置 spring: datasource: hikari: maximum-pool-size: ${DB_CORES * 2} connection-timeout: 3000 leak-detection-threshold: 60000关键公式:最大连接数 = (核心数 * 2) + (磁盘数 * 2)
6. 监控体系升级方案
6.1 新增监控指标项
# MDL等待时间监控 mysql_global_status_mdl_lock_wait_time{instance="$host"} # 长事务检测规则 - alert: LongRunningTransaction expr: mysql_processlist_time_seconds{state!="Sleep"} > 10 for: 2m6.2 智能熔断策略
基于历史数据训练的预测模型:
def should_circuit_break(): return (mdl_wait_time > threshold and active_transactions > cpu_cores * 3 and time.hour in business_peak_hours)这套系统在后续的618大促中成功拦截了3次潜在死锁风险