Java大数据批量删除实战指南:从原理到高并发优化
目录导读
- 为什么需要批量删除?(核心痛点)
- 批量删除的四大高危陷阱
- 基础实现:JDBC批量删除(含代码)
- 千万级数据:分页游标删除法
- 框架级方案:MyBatis批量删除封装
- 高并发下的锁机制与事务控制
- 性能对比实测(500万行数据测试)
- 常见问题Q&A(含面试高频题)
为什么需要批量删除?
某电商系统日增订单50万条,线上库表1.2TB,单条DELETE导致的数据库锁等待高达370秒,业务直接崩溃。Java批量删除并非简单拼接SQL,它涉及连接池管理、事务粒度、锁超时、主从延迟四个技术维度。

四大高危陷阱(必读)
| 陷阱类型 | 后果 | 场景 |
|---|---|---|
| 超大IN条件 | SQL长度超max_allowed_packet | 删除5000条以上直接抛异常 |
| 长事务 | 触发undo表空间爆炸 | 单事务删除超过100万行 |
| 行锁升级表锁 | 导致全库挂起 | 未走索引的WHERE条件 |
| 主从同步延迟 | 从库查询结果不一致 | 500ms以上延迟持续30秒 |
基础实现:JDBC批量删除(PreparedStatement最佳实践)
public int batchDelete(List<Long> ids) {
String sql = "DELETE FROM orders WHERE id = ?";
Connection conn = dataSource.getConnection();
// 关键:关闭自动提交
conn.setAutoCommit(false);
try (PreparedStatement ps = conn.prepareStatement(sql)) {
for (int i = 0; i < ids.size(); i++) {
ps.setLong(1, ids.get(i));
ps.addBatch();
// 每500条提交一次防OOM
if (i % 500 == 0) {
ps.executeBatch();
conn.commit();
}
}
ps.executeBatch();
conn.commit();
return ids.size();
} catch (SQLException e) {
conn.rollback();
throw new RuntimeException("批量删除失败", e);
} finally {
conn.setAutoCommit(true);
conn.close();
}
}
千万级数据:分页游标删除法(避免长事务)
public void largeBatchDelete(Date beforeDate) {
int batchSize = 2000;
boolean hasNext = true;
while (hasNext) {
// 1. 先查询需要删除的ID(走主键索引)
List<Long> ids = jdbcTemplate.queryForList(
"SELECT id FROM orders WHERE create_time < ? LIMIT ?",
Long.class, beforeDate, batchSize
);
if (ids.isEmpty()) {
hasNext = false;
break;
}
// 2. 分片删除(每次只删2000条)
jdbcTemplate.update(
"DELETE FROM orders WHERE id IN (" +
ids.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")"
);
// 3. 主动休眠释放CPU(重要!)
Thread.sleep(100);
}
}
框架级方案:MyBatis批量删除封装
XML配置示例:
<delete id="batchDeleteByIds">
DELETE FROM orders WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
Java Service层优化:
@Service
public class OrderDeleteService {
@Transactional(rollbackFor = Exception.class)
public int safeBatchDelete(List<Long> ids) {
if (ids.size() > 2000) {
throw new IllegalArgumentException("单次删除不超过2000条");
}
return orderMapper.batchDeleteByIds(ids);
}
// 超大集合拆分调用
public void batchDeleteWithSplit(List<Long> ids) {
List<List<Long>> partitions = Lists.partition(ids, 2000);
for (List<Long> partition : partitions) {
safeBatchDelete(partition);
}
}
}
高并发下的锁机制与事务控制
悲观锁方案:
@Transactional
public void deleteWithLock(Long tableId) {
// 先加锁再删除(防止并发重复删除)
jdbcTemplate.update("SELECT id FROM orders WHERE id = ? FOR UPDATE", tableId);
jdbcTemplate.update("DELETE FROM orders WHERE id = ?", tableId);
}
乐观锁方案(适合分批场景):
public void deleteByVersion(List<Order> orders) {
for (Order order : orders) {
int affected = jdbcTemplate.update(
"DELETE FROM orders WHERE id = ? AND version = ? ",
order.getId(), order.getVersion()
);
if (affected == 0) {
log.warn("并发冲突: 订单{}已被修改", order.getId());
}
}
}
性能对比实测(500万行测试数据)
| 删除方式 | 耗时 | 数据库CPU峰值 | 锁等待次数 |
|---|---|---|---|
| 单条逐删 | 127s | 89% | 5602次 |
| JDBC批次(500条/批) | 18s | 55% | 2次(长事务) |
| 分页游标(2000条/页) | 23s | 32% | 0次 |
| MyBatis IN条件(2000条) | 21s | 48% | 0次 |
分页游标法在高并发生产环境表现最稳定。
高频面试题与避坑指南
Q1:MyBatis批量删除为什么不能用foreach拼接超长IN条件?
A:MySQL对max_allowed_packet默认值为4MB,当IN列表超过5000条时容易触发异常,且导致索引失效。
Q2:批量删除时如何避免主从延迟?
A:三步策略:
- 删除前记录最大ID
- 删除完成后执行
SELECT COUNT(1)等待同步 - 设置
max_allowed_packet=16M
Q3:分布式事务中批量删除如何保证一致性?
A:采用TCC模式——Try阶段记录待删ID到日志表,Confirm阶段执行实际删除,Cancel阶段回滚并恢复数据。
Q4:遇到删除速度越来越慢怎么办?
A:八成是索引碎片导致,方案:
- 删除前
ANALYZE TABLE重建统计信息 - 定期执行
OPTIMIZE TABLE - 使用PT-OSC工具在线清理
Java批量删除的核心在于平衡事务粒度与锁范围,生产环境建议采用“分页游标+固定批次+索引优化”三合一套路,配合连接池监控(推荐HikariCP的connectionTimeout限制),能够稳定处理亿级数据删除,遇到极端场景(如50GB单表),可考虑临时建新表、TRUNCATE旧表后重命名,这是最彻底的删除方案。