同事埋了个坑:Insert into select语句把生产服务器炸了

同事埋了个坑:Insert into select语句把生产服务器炸了
最新回答
葫芦岛

2021-10-19 14:21:27

同事使用INSERT INTO SELECT语句导致生产服务器故障的核心原因在于全表扫描引发的锁表问题,最终引发业务中断。 以下是具体分析:

事故直接原因:全表扫描导致锁表
  • 默认加锁规则:在默认事务隔离级别下,INSERT INTO order_record SELECT * FROM order_today语句会对order_record加表锁,同时对order_today表逐行扫描并加锁(扫描到哪行锁哪行)。
  • 锁表过程:由于order_today表数据量庞大(700万且每日新增30万),且查询条件pay_success_time < '2020-03-08 00:00:00'未命中索引,MySQL被迫执行全表扫描。随着扫描行数增加,锁定的数据逐渐覆盖全表,最终导致整个表被锁死
  • 业务影响

    初期仅少量数据被锁,部分用户支付失败;

    随着锁范围扩大,大量用户支付失败、初始化订单失败;

    最终全表锁死,新订单无法插入,业务完全中断。

(全表扫描导致锁范围逐步扩大)根本原因:缺乏索引优化
  • 查询条件未命中索引:pay_success_time字段无索引,导致MySQL无法通过索引快速定位符合条件的记录,只能全表扫描。
  • 锁粒度失控:全表扫描使锁从“行级锁”退化为“表级锁”,直接阻塞所有并发写入操作(如新订单插入)。
解决方案:通过索引优化避免全表扫描
  1. 添加索引:为pay_success_time字段创建索引(如idx_pay_suc_time),使查询走索引而非全表扫描。
  2. 强制索引使用:在SQL中显式指定索引,确保优化器选择正确路径:INSERT INTO order_record SELECT * FROM order_today FORCE INDEX (idx_pay_suc_time) WHERE pay_success_time <= '2020-03-08 00:00:00';
  3. 执行效果:索引使查询仅锁定符合条件的记录,避免锁表,业务可正常并发写入。
(索引使锁范围仅限于目标记录)关键教训与预防措施
  1. 索引必要性

    使用INSERT INTO SELECT时,确保FROM表的查询条件有对应索引,避免全表扫描。

    定期分析慢查询日志,识别未命中索引的SQL并优化。

  2. 生产环境操作规范

    分批迁移:对大表数据迁移采用分批策略(如按ID范围或时间分段),减少单次锁表时间。

    低峰期操作:即使优化后,仍建议在业务低峰期执行高风险操作。

    监控与回滚:操作前确认监控告警阈值,准备快速回滚方案。

  3. 测试验证

    在预发环境模拟生产数据量,验证SQL执行计划及锁行为。

    使用EXPLAIN分析SQL是否走索引,避免全表扫描。

扩展建议:事务隔离级别与锁优化
  • 调整隔离级别:若业务允许,可考虑将事务隔离级别从REPEATABLE READ(MySQL默认)降为READ COMMITTED,减少间隙锁(Gap Lock)的影响。
  • 使用SELECT ... FOR UPDATE谨慎加锁:若需显式加锁,优先使用行级锁语句并控制范围。

此次事故的本质是对数据库锁机制和SQL执行计划理解不足,通过索引优化和操作规范可有效规避类似问题。