2. Batch概念解析:Batch的定义、Batch在数据库与消息队列中的应用场景

好,咱们正式开始聊Batch。很多做压测的朋友,一上来就调线程数、调连接池,结果调了半天吞吐量纹丝不动。为什么?说白了,你连Batch是什么都没搞清楚。

我个人习惯,讲任何技术概念之前,先给它一个最朴素的定义。Batch,就是「把多个小操作攒起来,一次性处理」。就这么简单。你想想看,去超市买东西,一件一件结账和把所有东西堆在收银台一次性结账,哪个快?显然是后者。Batch就是这个道理。

2.1 Batch的核心定义

从计算机系统的角度来看,Batch是一种批量处理模式。它把多个独立的请求或数据条目,打包成一个更大的单元,然后由处理端统一消费。

这里有几个关键参数,我列个表,你一眼就能看明白:

参数 含义 典型值
Batch Size 每次批量处理的数据条数 100、500、1000
Batch Interval 最长等待时间,超时即使没凑够也发 100ms、500ms、1s
Batch Buffer 存放待处理数据的缓冲区大小 64KB、1MB

嗯,这里要注意:Batch Size和Batch Interval是跷跷板关系。Size设太大,延迟会飙升;Interval设太短,Batch形同虚设。我在项目中遇到过有人把Interval设成10ms,结果Batch Size永远凑不够,每次都是单条发送,性能反而比非Batch模式还差。

核心结论:Batch的本质是用「延迟换吞吐」。你愿意等多久,决定了你能省多少系统开销。

2.2 Batch在数据库中的应用场景

数据库是Batch最经典的应用场景。我刚开始做性能调优那会儿,最常干的事就是盯着数据库的批量插入发呆。

2.2.1 JDBC Batch

Java开发者应该不陌生。JDBC的addBatch()和executeBatch(),就是典型的Batch实现。来看一段代码:

// 非Batch模式——每次提交一条
for (User user : userList) {
    PreparedStatement ps = conn.prepareStatement("INSERT INTO users VALUES (?, ?)");
    ps.setInt(1, user.getId());
    ps.setString(2, user.getName());
    ps.executeUpdate();  // 每次都要网络往返
}

// Batch模式——攒够100条再提交
PreparedStatement ps = conn.prepareStatement("INSERT INTO users VALUES (?, ?)");
int count = 0;
for (User user : userList) {
    ps.setInt(1, user.getId());
    ps.setString(2, user.getName());
    ps.addBatch();
    count++;
    if (count % 100 == 0) {
        ps.executeBatch();  // 一次网络往返处理100条
        count = 0;
    }
}
if (count > 0) {
    ps.executeBatch();  // 处理剩余不足100条的
}

我曾经在一个金融项目中,用这个简单的Batch优化,把数据库写入的TPS从2000直接拉到了35000。你想想看,17倍的提升,就改了几行代码。为什么?因为每次executeUpdate()都要走一次TCP握手、一次SQL解析、一次事务提交。Batch把这些开销平摊到了100条数据上,成本自然就降下来了。

避坑指南:我曾经把Batch Size设成10000,结果数据库直接OOM了。因为MySQL的max_allowed_packet默认只有4MB,一次塞太多数据包会被截断。建议从100开始试,逐步往上加,找到拐点。

2.2.2 数据库连接池中的Batch

除了SQL层面的Batch,连接池本身也有Batch的影子。比如HikariCP的maximumPoolSize和minimumIdle,本质上就是在控制「同时处理多少个请求的Batch」。只不过这个Batch是并发的,不是串行的。

2.3 Batch在消息队列中的应用场景

消息队列里的Batch,比数据库更讲究。因为消息队列通常要兼顾「生产者」和「消费者」两端。

2.3.1 生产者端Batch

以Kafka为例,它的生产者有一个batch.size参数。默认是16KB。什么意思?就是生产者会把发往同一个分区的消息攒起来,攒够16KB才真正发出去。

// Kafka生产者配置示例
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("batch.size", 32768);       // 32KB,默认16KB
props.put("linger.ms", 100);          // 最多等100ms
props.put("compression.type", "snappy"); // 压缩,进一步减少网络开销

这里有个细节:linger.ms就是前面说的Batch Interval。如果你把linger.ms设成0,那消息会立即发送,Batch形同虚设。我建议至少设成10-50ms,给Batch一点攒数据的时间。

注意:Kafka的Batch是分区级别的。如果你的Topic有10个分区,那每个分区都有自己的Batch缓冲区。这意味着总内存占用 = batch.size × 分区数。我曾经见过有人把batch.size设成1MB,分区数设成100,结果生产者直接用了100MB堆内存——GC压力瞬间爆炸。

2.3.2 消费者端Batch

消费者端的Batch,主要体现在fetch.min.bytes和max.poll.records上。

  • fetch.min.bytes:消费者拉取时,最少要拿到多少字节才返回。默认1字节,意味着有数据就返回。设大一点,比如1KB,可以减少拉取次数。
  • max.poll.records:一次poll最多返回多少条记录。默认500。设太大,处理时间会变长,可能导致消费者被踢出消费组。

我个人的经验是:消费者端的Batch Size,要跟你的业务处理耗时挂钩。如果每条消息处理需要10ms,那一次poll 500条就是5秒。如果消费组的max.poll.interval.ms默认是5分钟,那还好。但如果设的是30秒,你就得把max.poll.records降到200甚至100。

2.4 Batch的通用调优原则

讲了这么多,我总结几条通用的调优原则,你在任何场景下都能用:

  1. 先测基线,再调参数。不要一上来就改Batch Size。先跑一次非Batch模式,拿到基准值。
  2. Batch Size从100起步。不管是数据库还是MQ,100是个安全的起点。然后逐步翻倍:100→200→500→1000,观察吞吐量和延迟的变化。
  3. 关注拐点。当Batch Size增加到某个值后,吞吐量不再增长,甚至开始下降——那就是拐点。比如数据库可能因为锁冲突,MQ可能因为内存压力。
  4. Interval不要设太短。低于50ms的Interval,基本等于没开Batch。除非你的流量极大,每秒几万条,那10ms也能攒够。

一句话总结:Batch调参,调的不是参数本身,而是「延迟」和「吞吐」之间的平衡点。找到那个点,你的系统就能飞起来。

2.5 Batch知识体系图

下面这张图,我把Batch的核心概念和应用场景串起来了。你一看就明白:

Batch 批量处理 Batch Size Batch Interval Batch Buffer 数据库场景 消息队列场景 • JDBC Batch (addBatch) • 连接池批量复用 • 事务批量提交 • 生产者 batch.size • 消费者 max.poll.records • 压缩与批量发送 核心原则:延迟与吞吐的平衡

这张图把Batch的「三个参数」和「两个场景」串在了一起。你调参的时候,脑子里要有这张图:改Size影响什么?改Interval影响什么?Buffer设多大才不会爆内存?想清楚这些,调参就不会盲目。


好了,Batch的概念就讲到这里。说白了,Batch不是什么高深的技术,它就是「攒一波,干一票」。但就是这简单的思路,在压测中往往能带来几十倍的性能提升。下一节我们会聊具体的调参方法论,到时候拿实际数据说话。

课后小作业:打开你的项目,看看有没有地方是逐条发送的。试着加一个Batch逻辑,从Size=100开始,看看吞吐量变化了多少。我敢打赌,至少翻一倍。

可可精品资料库