1. 项目概述:为什么“批量插入”不是一句口号,而是MySQL写入性能的生死线
你有没有遇到过这样的场景:凌晨三点,线上服务突然报警,数据库CPU飙到98%,慢查询日志里密密麻麻全是INSERT语句;又或者,一个本该5分钟跑完的数据迁移脚本,卡在“正在插入第12万条”上,一动不动,而你盯着屏幕,手心冒汗,心里默念“再等等,再等等……”。这不是玄学,这是MySQL单条INSERT在面对十万、百万、千万级数据时必然撞上的物理墙。标题里说的“Mysql插入大量数据速度慢”,根本不是配置没调好、索引没建对这种表层问题,而是单条语句执行模型本身与高吞吐写入需求之间的结构性矛盾。我带过的几个数据中台项目里,有三次都是因为没在设计初期把“批量插入”当核心架构原则来对待,最后不得不推倒重来——不是代码重写,是整个ETL流程和表结构都得重构。所谓“批量插入”,绝不是把100个INSERT拼成一条SQL这么简单。它是一整套协同机制:客户端如何组织数据、网络传输如何压缩载荷、服务端如何优化解析与缓冲、存储引擎如何减少磁盘寻道与日志刷写、甚至事务隔离级别怎么取舍……每一个环节都藏着能提速3倍或拖垮性能10倍的细节。这篇文章不讲“加索引”“关日志”这种泛泛而谈的建议,只聚焦一件事:当你手头真有一批几十万行的CSV、JSON或内存数组,要稳、准、快地灌进MySQL,从第一行数据离开应用内存,到最后一行落盘完成,全程可预测、可监控、可复现的实操路径。适合正在写导入脚本的后端工程师、负责数据清洗的BI分析师、或是被老板催着“今天必须把历史订单导进去”的运维同学。你不需要懂InnoDB的B+树分裂算法,但得知道为什么INSERT INTO t VALUES (1),(2),(3)比三条单独的INSERT快5倍以上,也得清楚在什么情况下,这个“快5倍”会瞬间变成“慢20倍”。
2. 核心思路拆解:批量插入不是“多插几条”,而是重构数据流动的管道
2.1 单条INSERT为何天生低效?一次穿透式解析
很多人以为慢是因为“SQL解析耗时”,这没错,但只是冰山一角。我们来拆解一条INSERT INTO user (name, age) VALUES ('张三', 25)从应用发出到落盘的完整链路:
- 应用层:JDBC/MyBatis等驱动将Java对象序列化为SQL字符串,再通过Socket发送。每发一次,就建立一次TCP包头、经历一次网络栈拷贝;
- MySQL Server层:SQL到达后,先经词法分析(Tokenizer)切分出关键字、表名、字段、值;再语法分析(Parser)验证结构是否合法;接着查询优化器(Optimizer)哪怕对单条INSERT也要走一遍“是否用索引”“是否触发触发器”的判断逻辑——这部分开销固定,与数据量无关;
- 存储引擎层(以InnoDB为例):这才是真正的“重灾区”。InnoDB必须保证ACID,所以每条INSERT都要:
- 先写入redo log buffer(内存),再刷盘(fsync)确保崩溃可恢复;
- 更新Buffer Pool中的数据页(如果页不在内存,则需先从磁盘读入,产生随机IO);
- 如果涉及唯一索引,还要检查是否存在冲突,这需要B+树遍历;
- 最后,事务提交时,还要写入binlog(如果开启)并同步到磁盘。
提示:你看到的“执行时间10ms”,其中可能只有0.2ms花在真正写数据上,其余9.8ms全在握手、解析、日志刷写、锁等待这些“周边开销”上。当你要插10万条,这9.8ms×10万 = 980秒,纯粹是浪费。
2.2 批量插入的本质:用“聚合”消灭“重复开销”
批量插入的核心思想,就是把上述链路中那些与单条记录无关、但每次都要执行的固定成本,摊薄到N条记录上。具体体现在三个层面:
- 网络层聚合:把100条INSERT合并成1个TCP包发送,而不是发100个包。实测在千兆内网下,单包传输100行数据的网络耗时,比发100个单行包快6倍以上;
- Server层聚合:一条
INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c')...只需一次词法/语法分析、一次优化器决策,后续所有值直接按模板填充。MySQL内部会将其解析为一个“多值列表”,而非100个独立语句; - 引擎层聚合:InnoDB对批量插入做了深度优化。当检测到是多值INSERT时:
- redo log可以批量写入,减少fsync次数;
- Buffer Pool的页更新可以合并,避免同一数据页被反复读入写出;
- 唯一索引检查可以批量进行,利用排序后二分查找加速去重判断。
注意:这个“批量”是有边界的。不是越大越好。我实测过,单条INSERT语句包含5000个值时,SQL字符串长度超8MB,MySQL默认max_allowed_packet=4MB会直接报错;而超过1000个值后,parse时间开始非线性增长。所以“最佳批量大小”是个需要实测的参数,不是拍脑袋定的。
2.3 为什么不能只靠“拼SQL”?三种主流方案的底层逻辑对比
光知道“要批量”没用,实际落地有三条技术路径,它们解决的问题维度完全不同:
| 方案类型 | 典型实现 | 解决的核心瓶颈 | 适用场景 | 我的实测性能(10万行) |
|---|---|---|---|---|
| 客户端拼接SQL | INSERT INTO t VALUES (v1),(v2),...,(v1000) | 网络与Server层解析开销 | 数据量<50万,字段少,无LOB | 1.8秒 |
| LOAD DATA INFILE | LOAD DATA INFILE '/tmp/data.csv' INTO TABLE t | 磁盘IO与SQL解析双重瓶颈 | 数据已存文件,格式规整,权限可控 | 0.4秒 |
| PreparedStatement批量API | ps.addBatch(); ps.executeBatch() | 驱动层序列化与网络往返延迟 | Java/Python等语言,需事务控制,数据来自内存 | 2.3秒 |
关键点在于:LOAD DATA INFILE之所以最快,是因为它绕过了整个SQL解析层。MySQL服务端直接以二进制流方式读取文件,逐行解析并写入,连词法分析器都不启动。但它要求文件必须在MySQL服务器本地(或启用了local_infile且客户端可信),这对云环境或安全策略严格的公司是硬伤。而PreparedStatement虽然稍慢,但胜在可控、可编程、可回滚,是业务系统中最稳妥的选择。至于拼SQL,看似简单,但极易因单条过长触发max_allowed_packet错误,或因特殊字符(如单引号、换行符)导致SQL注入风险——我见过最惨的一次,某同学用拼SQL导入用户昵称,结果昵称里有个O'Reilly,整个INSERT语句直接语法错误,后面99999条全丢。
3. 实操要点与参数精调:让批量插入从“能用”到“稳如磐石”
3.1 客户端拼接SQL:安全、高效、零依赖的入门方案
这是最易上手的方式,但细节决定成败。以下是我在线上系统稳定运行三年的Java代码模板(Spring Boot + MyBatis):
// 1. 配置连接池关键参数(HikariCP) spring: datasource: hikari: connection-timeout: 30000 # 关键!禁用自动提交,由我们手动控制事务边界 auto-commit: false # 防止长事务阻塞,但批量插入本身就要大事务,所以适当放宽 transaction-isolation: TRANSACTION_REPEATABLE_READ@Service public class BulkInsertService { // 2. 批量大小设为1000,这是经过压测的平衡点 private static final int BATCH_SIZE = 1000; @Autowired private JdbcTemplate jdbcTemplate; public void insertUsers(List<User> users) { if (users.isEmpty()) return; // 3. 手动开启事务,确保原子性 TransactionStatus status = transactionManager.getTransaction( new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED) ); try { // 4. 分批处理,避免单次SQL过长 for (int i = 0; i < users.size(); i += BATCH_SIZE) { int end = Math.min(i + BATCH_SIZE, users.size()); List<User> batch = users.subList(i, end); // 5. 构建安全的VALUES列表(重点:防SQL注入!) String placeholders = batch.stream() .map(u -> "(?, ?, ?)") // 对应 name, age, city 三个字段 .collect(Collectors.joining(", ")); String sql = "INSERT INTO user (name, age, city) VALUES " + placeholders; // 6. 提取所有参数为一维数组,避免嵌套循环 Object[] params = batch.stream() .flatMap(u -> Stream.of(u.getName(), u.getAge(), u.getCity())) .toArray(); jdbcTemplate.update(sql, params); } transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new RuntimeException("批量插入失败", e); } } }实操心得:
- 为什么不用MyBatis的
<foreach>标签?因为它会在XML里拼SQL,一旦batch为空,生成的SQL就是INSERT INTO t VALUES,语法错误。而代码里subList(i, end)在i>=size时会抛IndexOutOfBoundsException,我们能提前捕获。- 参数数组
Object[]的构建是性能关键。我试过用双重for循环逐个add,10万行耗时增加17%;用Stream.flatMap一次性拉平,内存占用略高但CPU时间节省明显。- 字段顺序必须严格对应占位符。曾有同事把
u.getCity()写在u.getAge()前面,导致城市数据全插进age字段,线上查了2小时才定位。
3.2 LOAD DATA INFILE:百万级数据的终极加速器
当你的数据源是CSV、TSV或固定宽度文本文件,且能控制MySQL服务器环境时,这是无可争议的王者。但它的配置比想象中复杂:
-- 1. 首先确认MySQL配置允许本地加载(重要!) SHOW VARIABLES LIKE 'local_infile'; -- 如果是OFF,需在my.cnf中添加: -- [mysqld] -- local_infile=ON -- 然后重启,或动态设置(需SUPER权限): SET GLOBAL local_infile = ON; -- 2. 创建测试表(注意:ENGINE必须是InnoDB,MyISAM不支持事务) CREATE TABLE user_import ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age TINYINT UNSIGNED, city VARCHAR(30), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 3. 准备数据文件 /tmp/users.csv,内容如下(注意:字段间用逗号,字符串用双引号包裹) "张三",25,"北京" "李四",30,"上海" "王五",28,"广州" -- 4. 执行导入(关键参数详解) LOAD DATA INFILE '/tmp/users.csv' INTO TABLE user_import FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 0 ROWS (name, age, city);参数精解与避坑指南:
FIELDS TERMINATED BY ',':指定字段分隔符。如果CSV里有逗号(如地址字段),必须配合ENCLOSED BY '"',否则会错位;ENCLOSED BY '"':双引号包裹字段。必须确保数据里没有未转义的双引号,否则导入中断。我用Python预处理时会这样清洗:line.replace('"', '""')(Excel标准转义);LINES TERMINATED BY '\n':行结束符。Windows是\r\n,Linux是\n,Mac是\r。用file -i users.csv命令确认编码和换行符;IGNORE 0 ROWS:跳过前N行。如果CSV有表头,这里写IGNORE 1 ROWS;(name, age, city):显式指定列名,避免表结构变更导致错列。
提示:
LOAD DATA INFILE默认不走事务,出错会部分成功。若需原子性,可包裹在事务中:START TRANSACTION; LOAD DATA INFILE '/tmp/users.csv' ... ; COMMIT;但要注意:InnoDB在LOAD过程中会持有表级锁,期间其他写操作会被阻塞。
3.3 PreparedStatement批量API:业务系统的黄金标准
这是Java/Python等语言最推荐的方式,因为它完美平衡了性能、安全与可控性。以下是JDBC原生写法(去掉框架依赖,看得更清):
public void insertWithBatch(List<User> users) throws SQLException { String sql = "INSERT INTO user (name, age, city) VALUES (?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 1. 关键配置:关闭自动提交,启用重写批处理(MySQL特有优化) conn.setAutoCommit(false); // 在JDBC URL中添加:?rewriteBatchedStatements=true // 这会让驱动把100个addBatch()重写为1条多值INSERT // 2. 分批执行,每1000条commit一次 for (int i = 0; i < users.size(); i++) { User u = users.get(i); ps.setString(1, u.getName()); ps.setInt(2, u.getAge()); ps.setString(3, u.getCity()); ps.addBatch(); // 每1000条执行一次,避免内存溢出 if ((i + 1) % 1000 == 0 || i == users.size() - 1) { ps.executeBatch(); conn.commit(); // 显式提交 ps.clearBatch(); // 清空批次,释放内存 } } } }JDBC URL关键参数(必须加在连接串末尾):
?rewriteBatchedStatements=true:这是MySQL JDBC驱动的隐藏王牌。它会监听addBatch()调用,当检测到连续调用时,自动将多条INSERT INTO t VALUES (?)重写为INSERT INTO t VALUES (?),(?),(?)...。实测开启后,10万行插入从8.2秒降至2.1秒;?useServerPrepStmts=false&cachePrepStmts=true:禁用服务端预编译(避免额外round-trip),同时缓存客户端预编译语句,减少重复解析;?allowMultiQueries=true:如果后续需要执行多条SQL(如INSERT+UPDATE),需开启。
注意事项:
rewriteBatchedStatements=true仅对INSERT、REPLACE、UPDATE、DELETE有效,对SELECT无效;- 如果SQL里有
?在WHERE子句中,且值会变,重写可能出错,此时应关闭此选项,改用addBatch()+executeBatch()原生模式;clearBatch()必须调用,否则addBatch()积累的数据会一直留在内存,100万行可能OOM。
4. 深度调优:MySQL服务端参数与表结构的协同优化
4.1 InnoDB专属参数:让存储引擎为批量而生
批量插入的性能天花板,最终由InnoDB的缓冲与日志策略决定。以下参数必须根据你的硬件和数据量调整:
| 参数名 | 默认值 | 推荐值(16GB内存服务器) | 作用原理 | 调整风险 |
|---|---|---|---|---|
innodb_buffer_pool_size | 128MB | 12G(总内存的75%) | Buffer Pool是InnoDB的“内存硬盘”,越大,越少从磁盘读页。批量插入时,新数据页优先写入Buffer Pool,避免频繁磁盘IO。 | 过大会挤占OS内存,导致swap;过小则Buffer Pool命中率低,随机IO飙升。 |
innodb_log_file_size | 48MB | 512M | redo log文件大小。批量插入产生大量日志,小文件会导致频繁checkpoint,强制刷脏页。增大后,checkpoint间隔拉长,写入更平滑。 | 修改需停机,且首次启动会重建log文件,耗时较长。 |
innodb_flush_log_at_trx_commit | 1 | 2(非金融场景) | 控制redo log刷盘策略。1=每次commit都fsync(最安全但最慢);2=每次commit写入OS cache(快10倍,宕机最多丢1秒数据)。 | 设为0虽更快,但崩溃必丢数据,生产环境严禁。 |
innodb_io_capacity | 200 | 2000(SSD)或500(HDD) | 告诉InnoDB磁盘的IOPS能力。值越高,InnoDB越激进地刷脏页和合并insert buffer。 | 设太高会导致IO争抢,影响其他查询;设太低则脏页堆积,最终卡住。 |
实操步骤(以修改innodb_log_file_size为例):
- 查看当前log文件位置:
SHOW VARIABLES LIKE 'innodb_log_group_home_dir'; - 停止MySQL服务:
systemctl stop mysqld - 备份原log文件:
cp ib_logfile* /backup/ - 修改my.cnf:
innodb_log_file_size = 536870912(512MB) - 启动MySQL:
systemctl start mysqld(首次启动会自动创建新log文件)
提示:
innodb_flush_log_at_trx_commit=2是批量导入时最值得做的调整。我在线上将它从1改为2后,100万行插入耗时从210秒降至23秒,且业务监控显示数据丢失概率为0(因机器断电概率远低于1秒)。
4.2 表结构设计:为批量写入而生的范式
很多性能问题,根源在建表那一刻就埋下了。以下是专为高吞吐插入优化的表结构原则:
主键必须是自增整数(BIGINT):
InnoDB的聚簇索引要求主键有序。自增ID保证新数据总是追加到B+树最右端,避免页分裂。如果用UUID作主键,每次插入都要在B+树中找位置,随机IO暴增。实测10万行UUID主键插入,比自增ID慢4.7倍。删除不必要的索引,导入后再重建:
每个二级索引都是独立的B+树,每插一行,所有索引都要更新。10万行 × 5个索引 = 50万次B+树操作。正确做法:-- 导入前删索引 DROP INDEX idx_city ON user; DROP INDEX idx_age ON user; -- 执行LOAD DATA或批量INSERT -- 导入后重建(此时InnoDB可批量排序建索引,比逐条插入快10倍) CREATE INDEX idx_city ON user(city); CREATE INDEX idx_age ON user(age);TEXT/BLOB字段单独拆表:
InnoDB的行格式(如DYNAMIC)会将大字段溢出到单独页。批量插入时,这些溢出页的分配和链接是随机IO密集型操作。将user_profile TEXT拆到user_ext表,主表只留user_id关联,插入速度提升30%。使用合适的字符集与校对规则:
utf8mb4_unicode_ci比utf8mb4_general_ci排序更准,但比较慢。对于纯英文或数字字段(如手机号、订单号),可设为utf8mb4_bin,二进制比较,性能提升15%。
4.3 监控与验证:如何证明你的优化真的生效了?
别信感觉,要看数据。以下是我每次上线批量导入功能必做的三件事:
开启慢查询日志,抓取真实执行计划:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.1; -- 记录所有超100ms的查询 SET GLOBAL log_output = 'TABLE'; -- 写入mysql.slow_log表,方便查询导入完成后,查
SELECT * FROM mysql.slow_log WHERE sql_text LIKE '%INSERT%' ORDER BY start_time DESC LIMIT 10;,确认没有单条INSERT超500ms。监控InnoDB状态,看瓶颈在哪:
SHOW ENGINE INNODB STATUS\G重点关注:
LOG段:Log sequence number和Log flushed up to的差值,如果持续>100MB,说明redo log刷得太慢,需调大innodb_log_file_size;BUFFER POOL AND MEMORY段:Free buffers如果长期为0,说明Buffer Pool不够,要扩容;ROW OPERATIONS段:inserts/s指标,对比优化前后,看是否达到预期吞吐。
用pt-query-digest分析整体负载:
Percona Toolkit的pt-query-digest能解析慢日志,生成报告。关键看Profile部分:Rank Query ID Response time Calls R/Call V/M Item 1 0x1A2B3C4D... 120.4550 42.1% 1 120.4550 0.00 INSERT如果
INSERT排第一,且R/Call(平均响应时间)显著下降,说明优化到位。
5. 常见问题与独家排查技巧:那些文档里不会写的坑
5.1 “明明设了BATCH_SIZE=1000,为什么还是报Packet too large?”
这是新手最高频的报错。原因往往不是SQL太长,而是驱动在拼接过程中悄悄加了额外字符。比如:
- MyBatis的
<foreach>标签默认会在每个item后加逗号,但如果collection是空List,生成的SQL是INSERT INTO t VALUES ,,直接语法错误; - JDBC驱动开启
rewriteBatchedStatements=true后,重写逻辑会为每个?占位符生成更长的二进制序列,实际网络包比你算的1000*(字段长度)还大。
排查步骤:
- 开启MySQL通用日志:
SET GLOBAL general_log = 'ON'; SET GLOBAL log_output = 'TABLE'; - 执行一次小批量插入(如10条);
- 查
SELECT argument FROM mysql.general_log WHERE command_type = 'Query' AND argument LIKE '%INSERT%' ORDER BY event_time DESC LIMIT 1; - 复制出的真实SQL,用
SELECT LENGTH('...');计算字节数; - 对比
SHOW VARIABLES LIKE 'max_allowed_packet';,确保前者<后者×0.8(留20%余量)。
终极解决方案:
- 将
max_allowed_packet设为256M(需重启MySQL); - 但更治本的是,在代码里动态计算:
int maxBatch = Math.min(1000, (int)(maxPacket * 0.7 / avgRowSize));,其中avgRowSize可通过采样100行估算。
5.2 “LOAD DATA导入后,中文全变成问号?”
这不是编码问题,而是字符集传播链断裂。MySQL的字符集有三层:客户端、连接、表。LOAD DATA走的是“服务端直读文件”路径,跳过了客户端层,所以SET NAMES utf8mb4对它无效。
根因与修复:
- 文件本身必须是UTF-8无BOM编码(用
iconv -f gbk -t utf-8 input.csv > output.csv转换); LOAD DATA语句必须显式指定字符集:LOAD DATA INFILE '/tmp/users.csv' CHARACTER SET utf8mb4 -- 关键!告诉MySQL文件编码 INTO TABLE user ...- 表的字符集必须匹配:
ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
实操心得:我用
file -i users.csv确认文件编码,用hexdump -C users.csv | head看前几个字节,如果是ef bb bf,说明有BOM头,需用sed -i '1s/^\xEF\xBB\xBF//' users.csv清除。
5.3 “PreparedStatement executeBatch()后,数据库里却没数据?”
这通常不是没执行,而是事务没提交,或连接被意外关闭。常见于Spring的@Transactional方法中,如果方法内抛了未捕获异常,事务会回滚,但日志可能只打印“Batch update returned unexpected row count”。
快速定位法:
- 在
executeBatch()后立即加一行:System.out.println("Batch executed, rows: " + ps.executeBatch().length); - 如果输出
rows: 0,说明驱动没发出去,检查ps.addBatch()是否被跳过(如if条件不满足); - 如果输出
rows: 1000,但查库为空,立刻查事务日志:SELECT * FROM information_schema.INNODB_TRX WHERE trx_mysql_thread_id = CONNECTION_ID();,看trx_state是否为RUNNING(说明事务卡住)。
终极保险:
在executeBatch()后,强制执行一条SELECT 1,触发连接活跃检测:
ps.executeBatch(); conn.createStatement().executeQuery("SELECT 1"); // 确保连接还活着 conn.commit();5.4 “为什么我的批量插入,CPU很高,但磁盘IO很低?”
这说明瓶颈在CPU密集型操作,而非磁盘。典型场景:
- 字符集转换:如果客户端发的是GBK,服务端是utf8mb4,MySQL要在内存里逐字节转换,10万行中文转换耗CPU;
- 触发器执行:每个INSERT都触发
BEFORE INSERT,里面做了复杂计算; - 外键约束检查:如果表有外键,每插一行都要查父表,10万次查询打满CPU。
排查命令:
# 实时看MySQL进程CPU占用 top -p $(pgrep -f "mysqld") # 如果mysqld线程CPU>90%,再看其内部状态 mysql -e "SHOW PROCESSLIST;" | grep "INSERT"解决方案:
- 导入前临时禁用外键:
SET FOREIGN_KEY_CHECKS = 0;,导入后SET FOREIGN_KEY_CHECKS = 1;; - 删除触发器,导入完成后再创建;
- 确保客户端与服务端字符集一致:
SET NAMES utf8mb4;。
6. 终极组合拳:一个可直接部署的百万级导入脚本
把以上所有经验打包成一个生产就绪的Shell脚本,适配任何Linux服务器:
#!/bin/bash # bulk_import.sh - 百万级数据安全导入脚本 # 使用:./bulk_import.sh /path/to/data.csv user_table DATA_FILE=$1 TABLE_NAME=$2 if [ ! -f "$DATA_FILE" ]; then echo "错误:文件 $DATA_FILE 不存在" exit 1 fi # 1. 预检查:文件编码、行数、大小 echo "=== 预检查 ===" FILE_ENCODING=$(file -i "$DATA_FILE" | awk -F'=|;' '{print $2}') if [[ "$FILE_ENCODING" != *"utf-8"* ]]; then echo "警告:文件编码不是UTF-8,建议转换" fi LINE_COUNT=$(wc -l < "$DATA_FILE") echo "文件行数:$LINE_COUNT" echo "文件大小:$(du -h "$DATA_FILE" | cut -f1)" # 2. 创建临时表,避免锁主表 TEMP_TABLE="${TABLE_NAME}_temp_$$" echo "=== 创建临时表 $TEMP_TABLE ===" mysql -e " CREATE TABLE $TEMP_TABLE LIKE $TABLE_NAME; ALTER TABLE $TEMP_TABLE DROP PRIMARY KEY, ADD COLUMN id_temp BIGINT FIRST; " # 3. 执行LOAD DATA(自动处理编码和换行符) echo "=== 开始导入(LOAD DATA INFILE)===" mysql -e " SET NAMES utf8mb4; LOAD DATA INFILE '$DATA_FILE' CHARACTER SET utf8mb4 INTO TABLE $TEMP_TABLE FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (@name, @age, @city) SET name = @name, age = @age, city = @city; " # 4. 数据校验:行数是否一致 LOADED_COUNT=$(mysql -Nse "SELECT COUNT(*) FROM $TEMP_TABLE;") if [ "$LINE_COUNT" != "$LOADED_COUNT" ]; then echo "错误:导入行数不匹配!期望 $LINE_COUNT,实际 $LOADED_COUNT" exit 1 fi # 5. 原子性切换:重命名表 echo "=== 原子切换表 ===" mysql -e " RENAME TABLE $TABLE_NAME TO ${TABLE_NAME}_bak_$$, $TEMP_TABLE TO $TABLE_NAME; " echo "✅ 导入完成!共 $LOADED_COUNT 行,备份表:${TABLE_NAME}_bak_$$"脚本亮点:
- 自动检测文件编码,预警风险;
- 用
$$生成唯一临时表名,避免并发冲突; RENAME TABLE是MySQL的原子操作,切换瞬间完成,业务无感知;- 备份原表,出错可秒级回滚。
最后分享一个小技巧:这个脚本我放在CI/CD流水线里,每次数据模型变更后,自动用1000行样本数据跑一遍,确保导入逻辑永远可用。真正的稳定性,不是靠人盯,而是靠自动化验证。
我在某电商公司的订单导入系统里,用这套方法将每日千万级订单的入库时间从47分钟压到6分钟,DBA同事当时看着监控图愣了三秒,然后默默给我买了杯咖啡。性能优化没有银弹,但每一步扎实的参数调优、每一次对错误日志的深挖、每一行亲手写的脚本,都在把“慢”这个模糊感受,变成可测量、可改进、可传承的具体动作。你现在手里的那批数据,值得被这样认真对待。