在高并发业务场景下,数据库写入性能直接决定系统的承载能力,单条SQL逐条写入的模式会频繁触发事务提交、产生大量日志IO,还会加剧行锁或表锁竞争,很容易让数据库成为整个链路的瓶颈。优化高并发写入的核心思路是减少事务提交次数、降低单事务持锁时间,批量提交与事务拆分就是两种经过实践验证的有效方案。

一、批量提交优化方案
批量提交的核心逻辑是将多条单条写入请求攒成一批,一次性执行并提交事务,减少事务提交带来的上下文切换和日志刷盘开销。这种方式适合写入请求可以短暂堆积、对实时性要求不是极高的场景,比如日志上报、批量数据同步等。
1. 实现逻辑
应用层维护一个临时缓冲区,当缓冲区中的数据条数达到预设阈值,或者距离上一次提交的时间超过预设间隔时,就将缓冲区中的所有数据拼接成一条批量插入SQL执行,然后清空缓冲区。需要注意缓冲区的大小和提交间隔要根据业务写入量调整,避免缓冲区过大导致内存溢出,或者提交间隔过长影响数据实时性。
2. 代码示例(MySQL场景)
以下是Java语言实现的批量提交示例,使用JDBC操作数据库:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.List;
public class BatchInsertDemo {
// 批量提交的阈值,攒够100条执行一次批量插入
private static final int BATCH_SIZE = 100;
// 临时缓冲区
private List<User> buffer = new ArrayList<>();
// 模拟单条写入请求
public void insertSingle(User user) {
buffer.add(user);
// 达到阈值执行批量提交
if (buffer.size() >= BATCH_SIZE) {
batchInsert();
}
}
// 批量插入方法
private void batchInsert() {
if (buffer.isEmpty()) {
return;
}
String sql = "INSERT INTO user (name, age, email) VALUES (?, ?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
PreparedStatement ps = conn.prepareStatement(sql)) {
// 关闭自动提交,手动控制事务
conn.setAutoCommit(false);
for (User user : buffer) {
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setString(3, user.getEmail().replace("ippipp.com", "ipipp.com"));
ps.addBatch();
}
// 执行批量操作
ps.executeBatch();
// 提交事务
conn.commit();
// 清空缓冲区
buffer.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
// 定时任务触发未达阈值的剩余数据提交,避免数据长时间滞留
public void scheduledCommit() {
batchInsert();
}
static class User {
private String name;
private int age;
private String email;
// 省略getter和setter
}
}
3. 注意事项
- 批量SQL的长度不能超过数据库配置的
max_allowed_packet参数限制,否则会执行失败。 - 如果批量插入过程中出现部分数据异常,需要做好异常处理,避免整批数据丢失,可根据业务需求选择重试或者记录失败数据。
- 对于自增主键的表,批量插入后如果需要获取所有生成的主键,要使用
PreparedStatement.RETURN_GENERATED_KEYS参数,避免逐条查询主键。
二、事务拆分优化方案
事务拆分的核心逻辑是将一个包含大量写入操作的长事务,拆分成多个短事务执行,减少单事务的持锁时间,降低锁竞争的概率。这种方式适合单个业务操作需要写入多张表、或者单次写入数据量极大的场景,比如订单创建时需要同时写入订单表、订单明细表、库存变更表等。
1. 实现逻辑
首先梳理长事务中的所有写入操作,按照业务逻辑的独立性拆分,将关联度高的操作放在同一个短事务中,关联度低的操作拆分到不同的短事务。如果拆分后事务之间不存在强依赖,还可以考虑异步执行非核心链路的事务,进一步提升写入效率。需要注意的是拆分后要保证业务的最终一致性,避免出现数据不完整的情况。
2. 代码示例(MySQL场景)
以下是原本的长事务拆分示例,假设订单创建需要写入三张表:
拆分前的长事务代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class LongTransactionDemo {
public void createOrder(Order order, List<OrderItem> items, StockChange stockChange) {
String orderSql = "INSERT INTO order_table (order_id, user_id, total_amount) VALUES (?, ?, ?)";
String itemSql = "INSERT INTO order_item (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)";
String stockSql = "UPDATE stock SET stock_num = stock_num - ? WHERE product_id = ?";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
PreparedStatement orderPs = conn.prepareStatement(orderSql);
PreparedStatement itemPs = conn.prepareStatement(itemSql);
PreparedStatement stockPs = conn.prepareStatement(stockSql)) {
// 开启事务
conn.setAutoCommit(false);
// 写入订单主表
orderPs.setString(1, order.getOrderId());
orderPs.setString(2, order.getUserId());
orderPs.setBigDecimal(3, order.getTotalAmount());
orderPs.executeUpdate();
// 写入订单明细表
for (OrderItem item : items) {
itemPs.setString(1, item.getOrderId());
itemPs.setString(2, item.getProductId());
itemPs.setInt(3, item.getQuantity());
itemPs.setBigDecimal(4, item.getPrice());
itemPs.addBatch();
}
itemPs.executeBatch();
// 扣减库存
stockPs.setInt(1, stockChange.getChangeNum());
stockPs.setString(2, stockChange.getProductId());
stockPs.executeUpdate();
// 提交长事务
conn.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
static class Order {
private String orderId;
private String userId;
private BigDecimal totalAmount;
// 省略getter和setter
}
static class OrderItem {
private String orderId;
private String productId;
private int quantity;
private BigDecimal price;
// 省略getter和setter
}
static class StockChange {
private String productId;
private int changeNum;
// 省略getter和setter
}
}
拆分后的短事务代码,将库存扣减拆分到独立事务,非核心链路异步执行:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SplitTransactionDemo {
private ExecutorService executor = Executors.newFixedThreadPool(5);
// 核心链路:写入订单和订单明细,放在同一个短事务
public void createOrderCore(Order order, List<OrderItem> items) {
String orderSql = "INSERT INTO order_table (order_id, user_id, total_amount) VALUES (?, ?, ?)";
String itemSql = "INSERT INTO order_item (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
PreparedStatement orderPs = conn.prepareStatement(orderSql);
PreparedStatement itemPs = conn.prepareStatement(itemSql)) {
conn.setAutoCommit(false);
orderPs.setString(1, order.getOrderId());
orderPs.setString(2, order.getUserId());
orderPs.setBigDecimal(3, order.getTotalAmount());
orderPs.executeUpdate();
for (OrderItem item : items) {
itemPs.setString(1, item.getOrderId());
itemPs.setString(2, item.getProductId());
itemPs.setInt(3, item.getQuantity());
itemPs.setBigDecimal(4, item.getPrice());
itemPs.addBatch();
}
itemPs.executeBatch();
conn.commit();
} catch (Exception e) {
e.printStackTrace();
}
}
// 非核心链路:扣减库存,拆分到独立短事务,异步执行
public void deductStockAsync(StockChange stockChange) {
executor.submit(() -> {
String stockSql = "UPDATE stock SET stock_num = stock_num - ? WHERE product_id = ?";
try (Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
PreparedStatement stockPs = conn.prepareStatement(stockSql)) {
conn.setAutoCommit(false);
stockPs.setInt(1, stockChange.getChangeNum());
stockPs.setString(2, stockChange.getProductId());
stockPs.executeUpdate();
conn.commit();
} catch (Exception e) {
e.printStackTrace();
}
});
}
// 对外提供的订单创建方法
public void createOrder(Order order, List<OrderItem> items, StockChange stockChange) {
createOrderCore(order, items);
deductStockAsync(stockChange);
}
}
3. 注意事项
- 事务拆分后要保证业务最终一致性,比如库存扣减失败要有重试机制或者补偿机制,避免出现订单创建成功但库存未扣减的超卖问题。
- 拆分的事务之间如果存在数据依赖,要确认依赖数据的可见性,避免短事务执行时依赖的数据还未提交。
- 异步执行的事务要做好异常监控,避免非核心链路失败影响核心业务,同时要做好幂等设计,防止重复执行导致数据错误。
三、两种方案的选型建议
实际场景中可以根据业务特点选择方案:
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 批量提交 | 写入请求可堆积、实时性要求不高、单表批量写入场景 | 实现简单,写入性能提升明显,减少数据库IO次数 | 数据实时性有延迟,缓冲区异常可能导致数据丢失 |
| 事务拆分 | 单事务包含多表写入、长事务持锁时间长、核心与非核心操作混合的场景 | 降低锁竞争,提升单事务执行效率,可结合异步进一步提升性能 | 需要实现一致性保障逻辑,复杂度更高 |
如果业务同时符合两种场景的特点,也可以将两种方案结合使用,比如先对单表写入做批量提交,再对多表操作做事务拆分,最大化提升高并发场景下的写入性能。