Cassandra在写入模型上与传统关系型数据库差别很大,它针对单行级别的快速追加写入做了深度优化,因此很多初学者会想当然地把关系型数据库的批量操作经验照搬过来,直接用batch语句做大批量数据导入,结果性能反而比逐条写入还差。要正确使用Cassandra的batch批量写入,必须先理解它的设计意图:batch解决的是同一逻辑单元内多条操作的原子性问题,而不是用来提升写入吞吐量的批量导入工具。

一、batch的基本语法与两种类型
Cassandra中使用CQL语法定义batch,基本形式是把多条INSERT、UPDATE或DELETE语句放进BEGIN BATCH与APPLY BATCH之间。一个典型的写法如下:
BEGIN BATCH INSERT INTO users (user_id, name, city) VALUES (1001, 'zhangsan', 'beijing'); INSERT INTO users (user_id, name, city) VALUES (1002, 'lisi', 'shanghai'); UPDATE users SET city = 'guangzhou' WHERE user_id = 1001; DELETE FROM users WHERE user_id = 1002; APPLY BATCH;
Cassandra的batch分为两类:LOGGED batch(默认类型)和UNLOGGED batch。LOGGED batch会先把整个batch写入一张名为batchlog的系统表做持久化,保证即使部分节点故障,batch也能通过batchlog重放最终全部生效,代价是额外的写入开销和存储成本。UNLOGGED batch则跳过batchlog,性能更好,但不保证batch内所有操作的原子性,如果写入过程中部分节点失败,可能出现只有部分语句生效的情况。
写法上只需在BEGIN BATCH后面加上关键字即可切换类型:
BEGIN UNLOGGED BATCH INSERT INTO orders (order_id, user_id, amount) VALUES (5001, 1001, 99.9); UPDATE users SET city = 'shenzhen' WHERE user_id = 1001; APPLY BATCH;
需要注意,batch内不允许包含CREATE、ALTER这类DDL语句,也不建议混合对同一主键做有顺序依赖的修改,因为Cassandra不保证batch内部语句的执行顺序,所有操作本质上是对同一行最终状态的合并。
二、原子性、隔离性与适用场景
LOGGED batch提供的是all-or-nothing的原子性保证:要么batch内所有变更全部生效,要么全部不生效。这一点和关系型数据库的事务有相似之处,但它不提供跨行隔离性。也就是说,在batch执行过程中,其他并发读取请求可能看到batch中部分修改已经可见的中间状态。只有单行内的多次列更新才天然具有隔离性,因为Cassandra写入时会把对同一行的修改合并为一次原子写。
理解了这一点,就能推出batch真正合适的两类场景。第一类是对同一个分区内的多行做需要保持一致性的写入,例如一个订单表和它的明细表共用同一个分区键,把订单头和全部明细行放进同一个batch,可以保证它们要么一起出现要么一起消失。第二类是对同一行的多列做同时更新,比如把用户的姓名、地址、电话打包更新,batch能保证这些列变更的原子可见性。
下面是一个典型的正确用法示例,订单头和明细共享分区键order_id,整个batch落在同一个分区:
BEGIN BATCH INSERT INTO orders (order_id, line_no, product, qty) VALUES (9001, 1, 'keyboard', 2); INSERT INTO orders (order_id, line_no, product, qty) VALUES (9001, 2, 'mouse', 1); INSERT INTO orders (order_id, line_no, product, qty) VALUES (9001, 3, 'monitor', 1); APPLY BATCH;
反过来说,如果只是想批量插入几万条互相没有关系的数据,batch不仅没有收益,还会带来严重问题,这也是下一节要重点分析的内容。
三、跨分区batch的性能陷阱与正确的批量写入方案
batch最大的坑在于跨分区使用。当一个batch里的语句涉及多个不同分区时,协调节点需要把batch拆开,分别分发到各个分区所在的副本节点,整个batch的耗时取决于最慢的那个节点。更糟糕的是,LOGGED batch还要先把全部数据写入batchlog,相当于数据被写了两遍。官方文档明确建议单个batch应限制在同一分区内,并且默认配置batch_size_fail_threshold_in_kb(默认50KB)超过上限会直接拒绝执行,batch_size_warn_threshold_in_kb(默认5KB)超过则记录警告日志。
在大数据量导入场景下,正确的做法是使用异步并发写入来代替batch。以Python驱动为例,可以配合execute_async实现流水线式写入:
from cassandra.cluster import Cluster
from cassandra.query import BatchStatement
from concurrent.futures import wait
cluster = Cluster(['127.0.0.1'])
session = cluster.connect('mykeyspace')
insert_sql = session.prepare(
"INSERT INTO users (user_id, name, city) VALUES (?, ?, ?)")
futures = []
data = [(i, 'user_%d' % i, 'beijing') for i in range(100000)]
# 逐条提交异步写入请求,驱动会自动并发执行
for row in data:
futures.append(session.execute_async(insert_sql, row))
# 等待全部写入完成
wait(futures)
print("全部写入完成")这种模式下每个请求虽然独立发送,但驱动内部通过连接池和请求流水线把网络往返重叠起来,吞吐量远高于大batch。如果确实想在代码里使用batch语法减少请求次数,也应采用小批量策略:每个batch控制在几十到几百条、尽量落在同一分区内,并在多个batch之间保持并发提交,例如用多个线程各自发送自己的小batch。
此外还要注意prepared statement与batch的配合方式。如果batch中多条语句结构相同,可以复用一个prepared语句再配合BatchStatement使用,这样既能减少服务端解析开销,又能利用驱动对同分区batch的自动优化。若batch中的语句全部属于同一分区,一些驱动会自动将其作为UNLOGGED处理,省去batchlog开销。
四、参数调优与实践建议
涉及batch的服务端参数主要集中在cassandra.yaml配置文件中。在Windows环境下部署时,该文件位于安装目录的conf子目录下,例如C:\cassandra\conf\cassandra.yaml,可以用文本编辑器直接打开修改。除了前面提到的两个大小阈值外,还可以关注concurrent_writes参数,它控制写请求的并发线程数,默认32,在大规模批量导入时适当调高可以提升吞吐,但需要配合足够的CPU和磁盘IO能力。
实际使用中建议遵守以下几条原则。第一,永远不要把batch当作bulk loading工具,大批量数据迁移应使用Cassandra自带的COPY命令或专门的BulkLoader工具。第二,明确原子性需求:需要all-or-nothing语义就用默认的LOGGED batch,纯粹为了减少请求数且能接受部分失败就用UNLOGGED batch。第三,batch内的语句数量控制在两位数以内,数据总大小避免触碰5KB警告线。第四,监控日志中的batch size告警,如果频繁出现说明使用方式已经偏离了batch的设计初衷。
总结来说,Cassandra的batch批量写入是一把双刃剑:在同分区原子性写入场景下它是保证一致性的利器,在跨分区大批量导入场景下它则是性能杀手。理解它背后的分布式写入机制,把批量吞吐交给异步并发,把原子性交给LOGGED batch,才能把Cassandra的写入能力真正发挥出来。