1. 数据库大批量插入的性能痛点与实战优化方案
每次接手新系统时,我最怕遇到的就是历史数据迁移的场景。上周刚处理了一个案例:需要将800万条记录从旧系统迁移到新数据库,最初的方案是简单循环插入,结果整整跑了12个小时还没完成。这种场景下,常规的单条INSERT语句就像用吸管给游泳池注水,效率低得令人发指。
1.1 批量插入的三种武器库选择
JDBC批量操作是最基础的解决方案。通过Statement的addBatch()方法,我们可以将多条SQL语句打包发送。但要注意的是,MySQL的JDBC驱动默认情况下并不会真正批量执行,需要在连接字符串中添加rewriteBatchedStatements=true参数。这是我踩过的第一个坑——没有这个参数,批量插入速度可能比单条还慢。
java复制// 正确开启JDBC批量插入的示例
String url = "jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true";
try (Connection conn = DriverManager.getConnection(url);
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO users(name,age) VALUES(?,?)")) {
conn.setAutoCommit(false); // 关键步骤1:关闭自动提交
for (User user : userList) {
pstmt.setString(1, user.getName());
pstmt.setInt(2, user.getAge());
pstmt.addBatch(); // 关键步骤2:添加到批处理
if (i % 1000 == 0) {
pstmt.executeBatch(); // 关键步骤3:每1000条执行一次
conn.commit(); // 关键步骤4:手动提交
}
}
pstmt.executeBatch(); // 处理剩余记录
conn.commit();
}
LOAD DATA INFILE是MySQL的核武器级别方案。在我的测试中,500万条数据通过JDBC批量插入需要6分钟,而LOAD DATA INFILE仅需23秒。但要注意文件路径权限问题——MySQL服务必须有权限读取该文件,且secure_file_priv参数可能限制目录。
sql复制-- 文件格式要求:字段间用制表符分隔,行间用换行符分隔
LOAD DATA INFILE '/tmp/users.txt'
INTO TABLE users
FIELDS TERMINATED BY '\t'
LINES TERMINATED BY '\n';
多值INSERT语法是折中方案,适合中等规模数据(1万~50万条)。它的优势是SQL语句可读性强,且不需要特殊配置。但要注意单个SQL语句长度限制(max_allowed_packet参数,默认4MB)。
sql复制-- 多值INSERT示例(实际使用时建议每批1000条左右)
INSERT INTO users(name,age) VALUES
('张三',25),
('李四',30),
('王五',28);
1.2 事务处理的微妙平衡
批量操作必须配合事务才能发挥最大效果,但事务也不是越大越好。我曾遇到一个案例:把100万条插入放在单个事务中,结果不仅内存暴涨,最后还因锁等待超时失败。最佳实践是分批次提交,每批1000-5000条(根据数据大小调整)。
重要提示:Oracle的批量操作与MySQL完全不同,它需要设置executeBatch()的返回值处理。这是我用血泪换来的教训——没处理返回值可能导致部分数据丢失却无报错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主键冲突:雪花算法的美丽与哀愁
分布式ID生成是架构师的必修课。去年我们系统从单机切换到分布式,自增ID立即暴露出严重问题。雪花算法(Snowflake)看似完美——时间戳+机器ID+序列号,64位长整型,有序递增。但真实世界的时钟回拨问题,让多少工程师深夜惊醒。
2.1 雪花算法的致命弱点
机房ntp服务同步时间时,可能导致时钟回拨。我见过最夸张的回拨是7秒——直接导致生成的ID重复。解决方案有几种思路:
- 等待策略:检测到回拨时,线程休眠直到追回时间。适合回拨时间短的场景(<100ms)
- 扩展序列号:利用未使用的位扩展序列号空间。需要修改标准算法
- 异常机制:记录异常并告警,人工介入。适合关键业务
java复制// 带时钟回拨处理的改良版雪花算法
public synchronized long nextId() {
long currStamp = timeGen();
if (currStamp < lastStamp) { // 时钟回拨
long offset = lastStamp - currStamp;
if (offset <= 5) { // 小范围回拨,等待
Thread.sleep(offset * 2);
currStamp = timeGen();
} else { // 大范围回拨,抛异常
throw new RuntimeException("Clock moved backwards!");
}
}
// ...正常雪花算法逻辑
}
2.2 备选方案横向对比
当雪花算法不适用时,我们还有其他选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID | 简单,无冲突 | 无序,索引效率低 | 非关键数据 |
| Redis原子incr | 性能好,可控 | 依赖Redis | 中小规模分布式系统 |
| 数据库号段 | 简单可靠 | 有数据库访问开销 | 传统企业应用 |
| Zookeeper序列 | 严格有序 | 性能差,ZK成为瓶颈 | 已用ZK的系统 |
我的经验法则:TPS<2000用号段模式,2000-5000考虑Redis,更高并发再上雪花算法(但要处理好回拨)。
3. SQL监控双雄:p6spy与Druid的攻防战
慢SQL是系统性能的隐形杀手。去年我们一个核心接口响应时间从200ms暴涨到3秒,最终定位到一个没有索引的联表查询。好的监控工具能让你在用户投诉前发现问题。
3.1 p6spy的间谍之道
p6spy通过JDBC驱动代理机制,可以记录所有SQL执行细节。配置很简单,但有几个隐藏技巧:
- 在
spy.properties中设置logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat可以自定义输出格式 excludecategories=info,debug可以过滤掉非SQL日志- 生产环境一定要设置
filter=true,否则日志量会爆炸
properties复制# spy.properties关键配置
driverlist=com.mysql.jdbc.Driver
dateformat=yyyy-MM-dd HH:mm:ss
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
appender=com.p6spy.engine.spy.appender.Slf4jLogger
filter=true
excludecategories=info,debug
3.2 Druid的监控之道
Druid自带的监控界面是其杀手锏。除了基本配置,这几个参数对性能影响很大:
timeBetweenLogStatsMillis:统计日志输出间隔(默认300000ms)maxActive:连接池大小(建议20-50)validationQuery:连接有效性检查SQL(MySQL用select 1)
xml复制<!-- Druid数据源配置示例 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${db.url}"/>
<property name="username" value="${db.user}"/>
<property name="password" value="${db.pass}"/>
<property name="filters" value="stat,wall"/>
<property name="maxActive" value="20"/>
<property name="initialSize" value="1"/>
<property name="maxWait" value="60000"/>
<property name="timeBetweenEvictionRunsMillis" value="3000"/>
<property name="validationQuery" value="SELECT 1"/>
</bean>
3.3 监控指标的三重境界
- 基础层:执行时间、执行次数(p6spy和Druid都能提供)
- 中间层:慢SQL告警、执行计划分析(Druid的WallFilter)
- 高级层:SQL模板分析、参数化查询统计(需要定制开发)
我习惯在测试环境用p6spy做全量SQL审计,生产环境用Druid做轻量级监控。两者配合可以覆盖大部分场景。
4. 实战中的组合拳:从监控到优化的闭环
去年双十一前,我们通过监控发现一个商品查询接口的TP99从50ms涨到了200ms。排查过程堪称教科书案例:
- Druid监控显示该接口SQL执行次数异常高
- p6spy日志显示有大量相似SQL,只是参数不同
- 分析后发现是MyBatis循环生成SQL导致
- 改用批量查询后,性能提升8倍
这个案例让我深刻理解:监控只是手段,优化才是目的。好的监控要能指导优化决策。
4.1 优化决策树
面对性能问题,我总结出这样的决策流程:
code复制是否高频查询?
├─ 是 → 是否走索引?
│ ├─ 是 → 检查索引效率
│ └─ 否 → 加索引
└─ 否 → 是否大数据量?
├─ 是 → 考虑分页或异步
└─ 否 → 检查连接池配置
4.2 那些年我踩过的坑
-
连接池泄漏:没有正确关闭Connection导致连接耗尽。解决方案是用
@Transactional管理事务,或用try-with-resources语法。 -
错误的分页:用
LIMIT 100000,20查询大量数据。应该改用WHERE id > last_id LIMIT 20的游标方式。 -
过度索引:在一个频繁更新的表上建了6个索引,导致写入性能下降。最终通过索引合并解决了问题。
数据库优化就像中医调理,需要望闻问切。监控工具是听诊器,批量操作是速效药,而索引和事务控制则是日常保健。没有银弹,只有对症下药。
