Cassandra的batch语句分为logged batch和unlogged batch两种,很多刚接触的开发者会误以为unlogged batch更简单就更应该随便用,结果线上出现了大量超时和性能抖动。unlogged batch本质上只是把多条语句打包在一个请求里发送,省去了logged batch需要维护的batchlog机制,因此它不具备原子性保证,但换来的是更低的服务端开销。理解它的适用边界,是用好它的第一步。

unlogged batch的底层原理
当客户端提交一个logged batch时,Cassandra会先把整个批次写入一张系统表batchlog(位于system键空间),作为故障恢复的依据,然后再执行批次中的各条语句。如果某个节点在执行中途宕机,协调者可以利用batchlog重放整个批次,从而保证all-or-nothing的原子性。这个额外的写放大是logged batch代价高昂的根本原因。
而unlogged batch完全跳过了batchlog这一步。使用BEGIN UNLOGGED BATCH语法后,协调者只是简单地把批次里的每一条mutation分发到对应的副本节点,每条语句独立执行、独立生效。这意味着如果批次执行到一半失败,前面已经写入的数据不会回滚,你拿到的可能是部分成功的状态。
从网络层面看,unlogged batch的价值在于把N次网络往返压缩成1次。对于一次插入100条记录的场景,逐条发送需要100个请求的序列化和排队开销,而一个unlogged batch只需要一次请求。但要注意,这只是网络层面的优化,服务端每条语句的写入成本一点都没有减少,尤其是跨分区的批次,每个分区仍然要独立落盘。
与logged batch及逐条写入的对比
三种写入方式的差异可以用一张表来概括:
| 写入方式 | 原子性 | 网络往返 | 服务端开销 | 适用场景 |
|---|---|---|---|---|
| logged batch | 有 | 1次 | 高,需要写batchlog | 需要多表一致写入 |
| unlogged batch | 无 | 1次 | 中,逐条执行 | 同分区大批量写入 |
| 逐条异步写入 | 无 | N次 | 低 | 高并发分散写入 |
官方文档反复强调一个原则:batch的正确用途是同一分区的批量写入,而不是为了减少网络往返而盲目把不同分区的数据打包在一起。当批次中所有语句的主键都属于同一个分区时,服务端可以把这些mutation合并成一次顺序写,磁盘IO效率接近单条写入;一旦跨分区,协调者就要拆分批次、分别路由,批次越大,协调者节点的内存和CPU压力越大,很容易触发写超时。
Java驱动中的正确写法
下面演示DataStax Java驱动中构建unlogged batch的标准方式,注意所有语句的主键都落在同一个分区键上:
// 假设表结构:CREATE TABLE sensor_data (
// sensor_id uuid, recorded_at timestamp, value double,
// PRIMARY KEY (sensor_id, recorded_at));
UUID sensorId = UUID.randomUUID();
BatchStatement batch = BatchStatement.newInstance(
BatchStatement.Type.UNLOGGED);
for (int i = 0; i < 50; i++) {
SimpleStatement stmt = SimpleStatement.newInstance(
"INSERT INTO sensor_data (sensor_id, recorded_at, value) VALUES (?, ?, ?)",
sensorId,
Instant.now().plusSeconds(i),
Math.random() * 100
);
batch = batch.add(stmt);
}
session.execute(batch);
这里批次内50条语句的分区键都是同一个sensorId,它们会被合并为对单个分区的顺序写,这正是unlogged batch最能发挥价值的场景。反例是把50个不同sensor的数据塞进一个批次,这种写法在生产上几乎必然带来延迟尖刺。
如果确实需要批量写入大量跨分区的数据,更好的做法是使用异步执行配合并发控制,而不是依赖batch:
// 使用限流的异步写入替代跨分区batch
Semaphore semaphore = new Semaphore(1000);
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (SensorReading reading : readings) {
semaphore.acquire();
futures.add(session.executeAsync(
buildInsertStatement(reading))
.whenComplete((rs, ex) -> semaphore.release())
.toCompletableFuture());
}
CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])).join();
这种模式下每个请求独立失败、独立重试,吞吐能力和稳定性都远好于巨型跨分区batch。如果使用的是配置文件驱动的应用,驱动参数一般在应用的配置目录中调整,例如Windows环境下可能位于C:\app\config\cassandra-driver.yaml,其中可以设置最大并发请求数和超时时间。
常见陷阱与调优建议
第一个陷阱是批次大小失控。unlogged batch没有原子性保护,但不代表可以无限大,单个批次的大小受限于请求帧大小限制(默认约为256MB,但实际建议控制在几百条以内),同时批次中每条语句在协调者内存里都要占用缓冲,过大的批次会直接拖垮协调节点。建议在客户端层面做分片,每100到500条切一个批次。
第二个陷阱是重试语义混乱。由于批次不是原子的,一条语句失败后简单重试整个批次,可能导致已经成功的语句被重复写入。如果你的写入是幂等的(相同主键覆盖相同值),这没有问题;否则应该记录失败的具体语句,只重试失败部分,或者干脆改用逐条异步写入让驱动自己处理幂等重试策略。
第三个陷阱是把unlogged batch和prepared statement的关系搞混。batch里添加的语句本身可以是prepared语句,这是推荐做法,能减少服务端解析开销。另外要留意DEFAULT UNLOGGED BATCH的一个细节:在不加关键字的情况下,很多驱动默认生成的是logged batch,如果你明确不需要原子性,一定要显式声明UNLOGGED,否则白白承担batchlog的写放大成本。
总结一下:unlogged batch是同分区批量写入的利器,是跨分区写入的隐形炸弹。判断标准很简单,看批次内所有语句是否落在同一个分区键上,是就用它,不是就换异步逐条写入。配合合理的批次分片和幂等设计,就能在Cassandra上获得稳定的高吞吐写入能力。
Cassandraunlogged batch批量写入修改时间:2026-08-31 01:44:40