ScyllaDB CQL BATCH 语句完全指南:语法、原子性、批处理日志与强一致性
2026/9/14 14:53:10 网站建设 项目流程

ScyllaDB CQL BATCH 语句完全指南:语法、原子性、批处理日志与强一致性

【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb

BATCH 是 ScyllaDB 的 CQL 中用于将多条INSERTUPDATEDELETE语句组合成单条请求执行的语句,其完整规范定义在 docs/cql/dml/batch.rst。本指南围绕该文档展开,深入讲解 BATCH 的语法、原子性语义、LOGGED/UNLOGGED/COUNTER 三种类型的区别、强一致性表上的特殊行为,并结合仓库源码(cql3/Cql.g、cql3/statements/batch_statement.cc、db/batchlog_manager.cc)剖析其底层实现与实战配置。读完本文,你将能够正确地在单分区与多分区场景下选用合适的 BATCH 类型,理解批量写入的原子性边界与性能权衡,并掌握批量大小阈值等相关配置项。

BATCH 语法与示例

语法定义

BATCH 语句的完整文法定义如下:

batch_statement: BEGIN [ UNLOGGED | COUNTER ] BATCH : [ USING `update_parameter` ( AND `update_parameter` )* ] : `modification_statement` ( ';' `modification_statement` )* : APPLY BATCH modification_statement: `insert_statement` | `update_statement` | `delete_statement`

其 ANTLR 文法实现在 cql3/Cql.g 中:BEGIN后可选地跟随UNLOGGEDCOUNTER关键字(默认类型为LOGGED,对应cql3::statements::raw::batch_statement::type::LOGGED),随后是可选USING子句、一条或多条修改语句(由batchStatementObjective限制为insertStatementupdateStatementdeleteStatement三者之一,见 cql3/Cql.g),最后以APPLY BATCH结束。

示例

官方文档给出了一个跨多个分区、混合三种修改语句的经典示例:

BEGIN BATCH INSERT INTO users (userid, password, name) VALUES ('user2', 'ch@ngem3b', 'second user'); UPDATE users SET password = 'ps22dhds' WHERE userid = 'user3'; INSERT INTO users (userid, password) VALUES ('user4', 'ch@ngem3c'); DELETE name FROM users WHERE userid = 'user1'; APPLY BATCH;

注意示例中并未显式指定 BATCH 类型,因此这是一个 LOGGED 批处理。

BATCH 的核心用途

BATCH 将多条修改语句组合为一条语句,主要解决三个问题:

  • 减少网络往返:原本需要客户端与服务器(以及有时服务器协调节点与副本之间)多次交互的多个更新,现在一次请求即可完成。
  • 单分区原子性:同一分区键(partition key)下的所有更新在 BATCH 中被原子地执行。
  • 默认的最终一致性保证:默认情况下,批处理中的所有操作以logged(记录日志)方式执行,确保所有变更最终全部完成,或一个都不完成(即 all-or-nothing),详见下文 UNLOGGED 批处理 一节。

文档同时强调了几点重要约束:

  • BATCH 中只能包含UPDATEINSERTDELETE语句,不能嵌套其他 BATCH。
  • BATCH不是SQL 事务的完整等价物。
  • 如果每条操作未显式指定时间戳,则所有操作将使用同一个时间戳(自动生成或 BATCH 级别提供)。由于 ScyllaDB 在时间戳相同时的冲突解决机制,操作的实际生效顺序可能与 BATCH 中列出的顺序不同。若要强制特定顺序,必须为每条操作分别指定时间戳。这一点与 docs/cql/dml.rst 中"更新排序(Update ordering)"一节描述的时间戳冲突解决算法一致:cell 级别以时间戳最大者获胜,时间戳相同时依据 cell 是否为 tombstone、是否过期、TTL 以及值本身等属性做一致的冲突裁决。
  • 单分区的 LOGGED 批处理会被自动优化为 UNLOGGED 批处理,以消除不必要的批处理日志开销(下文源码分析会展示这一优化在 cql3/statements/batch_statement.cc 中的体现)。

USING 子句与更新参数

BATCH 支持USING子句携带更新参数,其中TIMESTAMP的语义与UPDATE语句中的TIMESTAMP参数完全相同,TIMEOUT同样受支持(TTL不被允许,见下文校验部分)。update_parameter的完整定义位于 docs/cql/dml.rst:

update_parameter: ( TIMESTAMP `int_value` | TTL `int_value` | TIMEOUT `duration` )

各参数的语义:

  • TIMESTAMP:为操作设置时间戳。若未指定,协调节点会在语句开始执行时使用当前时间(自 Unix 纪元 1970-01-01 00:00:00 UTC 起,以微秒为单位)作为时间戳。同一协调节点上生成的查询时间戳保证唯一(即使跨该节点上的不同 shard),但不同节点分配的时间戳不保证全局唯一;在高写入速率下时间戳碰撞并不罕见,碰撞时由冲突解决算法决定哪个插入的 cell 胜出。
  • TTL:为插入值指定可选的生存时间(秒),到期后自动删除。TTL 作用于插入的值本身而非列,后续对列的更新会重置 TTL。默认永不过期;TTL 为 0 等价于无 TTL;若表配置了default_time_to_live,TTL 为 0 会移除插入或更新值的 TTL。
  • TIMEOUT:为特定请求指定超时时长。

源码中的参数校验

USING子句的参数在 cql3/statements/batch_statement.cc 的batch_statement::validate()中做了严格约束,这些约束直接决定了哪些组合合法:

  • BATCH 级别不允许设置全局 TTL:抛出"Global TTL on the BATCH statement is not supported."
  • 条件 BATCH(含IF子句)不允许自定义时间戳:抛出"Cannot provide custom timestamp for conditional BATCH"
  • COUNTER 批处理不允许自定义时间戳:抛出"Cannot provide custom timestamp for counter BATCH"
  • 含计数器语句的批处理不允许自定义时间戳:抛出"Cannot provide custom timestamp for a BATCH containing counters"
  • 时间戳要么在 BATCH 级别设置,要么在单条语句级别设置,二者不能混用:抛出"Timestamp must be set either on BATCH or individual statements"
  • COUNTER 批处理中不允许包含非计数器语句:抛出"Cannot include non-counter statement in a counter batch"
  • LOGGED 批处理中不允许包含计数器语句:抛出"Cannot include a counter statement in a logged batch"
  • 计数器与非计数器变更不能出现在同一批处理中:抛出"Counter and non-counter mutations cannot exist in the same batch"
  • 带条件的 BATCH 不能跨表:抛出"BATCH with conditions cannot span multiple tables";执行阶段还会校验不能跨分区(见 cql3/statements/batch_statement.cc)。

批处理超时

批处理的超时由其类型决定:COUNTER批处理使用counter_write_timeout,其余类型使用write_timeout(见 cql3/statements/batch_statement.cc 的timeout_for_type())。若USING中显式设置了TIMEOUT,则优先采用该值(cql3/statements/batch_statement.cc)。

LOGGED(默认)与 UNLOGGED 批处理

LOGGED 批处理与批处理日志

默认情况下,ScyllaDB 使用批处理日志(batch log)来保证批处理中的操作要么全部最终完成,要么一个也不完成。需要强调的是,这种隔离性仅限同一分区内——跨分区的 LOGGED 批处理虽然通过批处理日志保证最终全部完成(或全部回滚),但操作之间并不具备事务隔离。

批处理日志的实现在 db/batchlog_manager.cc 中:批处理写入时先在system.batchlog表中记录一条包含所有 mutation 的日志项(get_batchlog_mutation_for),随后将实际 mutation 发送到各副本;副本投递完成后删除日志项(get_batchlog_delete_mutation)。日志项按written_at时间散列到 256 个批处理日志分片(batchlog_shard_bits = 8,见 db/batchlog_manager.cc),并支持 v1/v2 两种 schema 版本及在线迁移(maybe_migrate_v1_to_v2)。后台的batchlog_replay_loop()会周期性扫描尚未完成的日志项并重新投递(do_batch_log_replay),从而保证最终一致性;当副本返回失败时,日志项会被标记为failed_replay阶段以便后续重试。

多分区批处理的性能代价与 UNLOGGED 选项

当批处理跨越多个分区时,批处理日志会带来明显的性能开销。如果不想承担这一代价,可以使用UNLOGGED选项跳过批处理日志。代价是:使用UNLOGGED后,失败的批处理可能只被部分应用(partly applied)。

BEGIN UNLOGGED BATCH UPDATE users SET password = 'ps22dhds' WHERE userid = 'user3'; INSERT INTO users (userid, password) VALUES ('user4', 'ch@ngem3c'); APPLY BATCH;

源码中的三种原子性路径

cql3/statements/batch_statement.cc 的execute_without_conditions()清晰地展示了三种情形:

  1. 类型非LOGGED(即UNLOGGED):mutate_atomic = false,直接非原子写入,并累计batches_pure_unlogged统计。
  2. 类型为LOGGED且产生的 mutation 数量多于 1:mutate_atomic = true,走批处理日志路径,累计batches_pure_logged统计。
  3. 类型为LOGGED但 mutation 数量只有 1(单分区优化):自动降级为非原子写入(mutate_atomic = false),累计batches_unlogged_from_logged统计——这正是文档所述"单分区的 LOGGED 批处理会被转换为 UNLOGGED 批处理"优化在实现层面的体现。

所有路径最终都经由qp.proxy().mutate_with_triggers(...)提交给存储代理执行。

批处理大小阈值配置

ScyllaDB 会对批处理大小做守卫(guardrail),相关阈值配置在 conf/scylla.yaml 中,参数定义于 db/config.hh:

# Log WARN on any batch size exceeding this value. 128 kiB per batch by default. # Caution should be taken on increasing the size of this threshold as it can lead to node instability. batch_size_warn_threshold_in_kb: 128 # Fail any multiple-partition batch exceeding this value. 1 MiB (8x warn threshold) by default. batch_size_fail_threshold_in_kb: 1024
  • batch_size_warn_threshold_in_kb:默认 128 KiB,超过该值记录 WARN 日志。
  • batch_size_fail_threshold_in_kb:默认 1024 KiB(即 1 MiB,为警告阈值的 8 倍),超过该值的多分区批处理直接失败。

在 cql3/statements/batch_statement.cc 的verify_batch_size()中:只有当批处理产生的 mutation 数量大于 1 时才做检查;累计各 mutation 分区的外部内存占用,若超过batch_size_fail_threshold_in_kb则抛出"Batch too large"异常,若超过警告阈值则记录包含具体大小与超出量的 WARN 日志。对应测试可见 test/cqlpy/test_batch.py:构造超过 1025 KiB 的批处理并断言抛出InvalidRequest,错误信息为"Batch too large"

COUNTER 批处理

计数器(counter)更新与其他更新不同,不是幂等的。因此批处理计数器更新时必须使用COUNTER选项:

BEGIN COUNTER BATCH UPDATE click_stats SET clicks = clicks + 1 WHERE page_id = 1; UPDATE click_stats SET clicks = clicks + 1 WHERE page_id = 2; APPLY BATCH;

COUNTER 批处理受以下限制(来自源码校验与文档):

  • COUNTER 批处理中不能混合非计数器语句,反之亦然(cql3/statements/batch_statement.cc)。
  • LOGGED 批处理中不能包含计数器语句(cql3/statements/batch_statement.cc)。
  • COUNTER 批处理不支持自定义时间戳(cql3/statements/batch_statement.cc)。
  • 同一批处理中不能混合原始(raw)与常规计数器 shard 写(cql3/statements/batch_statement.cc)。
  • 强一致性表不支持 COUNTER 批处理(见下文)。

强一致性表中的 BATCH

当 BATCH 针对强一致性表(strongly consistent table,通过 Raft 共识协议提供线性化保证的表)时,行为与普通表不同:

  • 批处理中所有语句必须针对同一分区。执行阶段在 cql3/statements/strong_consistency/batch_statement.cc 中逐条构建分区键并校验:每条语句必须恰好定位单个分区(否则抛出"Each statement in a strongly consistent batch must target a single partition"),且所有语句的分区键必须相同(否则抛出"All statements in a strongly consistent batch must target the same partition");校验阶段还要求所有语句针对同一张表(cql3/statements/strong_consistency/batch_statement.cc)。
  • mutation 被合并为单个 mutation,并通过 Raft 原子提交:在 cql3/statements/strong_consistency/batch_statement.cc 中,协调器把批处理内各语句产生的 mutation 依次apply合并为单个 mutation,再调用强一致性协调器的mutate()经由 Raft 提交,最终获得原子的 all-or-nothing 语义。
  • LOGGED 与 UNLOGGED 在此场景下没有区别:原子性始终由底层 Raft 共识协议保证,而非批处理日志。两种写法都允许,效果相同。
  • COUNTER 批处理不支持强一致性表:校验阶段直接抛出"Counter batches are not supported with strongly consistent tables"(cql3/statements/strong_consistency/batch_statement.cc)。

此外,从 cql3/statements/batch_statement.cc 可以看到,语句在 prepare 阶段即根据 keyspace 是否启用强一致性分流:若目标 keyspace 是强一致性的,则构造strong_consistency::batch_statement,否则构造普通的cql3::statements::batch_statement

条件批处理(LWT BATCH)

尽管原文档未展开,但源码揭示了 BATCH 与轻量级事务(LWT)结合的一个重要细节:若批处理中任意语句带有IF条件子句,则整个批处理被视为 CAS(Compare-And-Set)批处理(_has_conditions标记,见 cql3/statements/batch_statement.hh),执行时走 Paxos 路径(cql3/statements/batch_statement.cc)。CAS 批处理有如下约束与行为:

  • 不能跨表(校验阶段抛出"BATCH with conditions cannot span multiple tables"),执行阶段也不能跨分区(抛出"BATCH with conditions cannot span multiple partitions")。
  • 不能自定义时间戳("Cannot provide custom timestamp for conditional BATCH")。
  • 结果集固定包含[applied]列及所有主键列(见build_cas_result_set_metadata(),cql3/statements/batch_statement.cc);与 Cassandra 不同,ScyllaDB 无论 CAS 成功与否都返回完整列结果集,方便客户端统一 prepare。
  • 统计指标cas_batchesstatements_in_cas_batches会相应累加。

实战建议与最佳实践

综合文档与源码,使用 BATCH 时应遵循以下原则:

  1. 优先单分区批处理:BATCH 最擅长的场景是把针对同一分区键的多次写入合并成一次请求。这既减少网络往返,又能获得真正的原子性,且 LOGGED 批处理还会自动降级为 UNLOGGED 以消除批处理日志开销。
  2. 跨分区时权衡原子性与性能:跨多分区的 LOGGED 批处理依赖批处理日志,存在明显的性能代价;UNLOGGED可跳过日志但允许部分应用。只有当你确实需要"最终全部完成或全部不完成"的保证时才使用 LOGGED 跨分区批处理。
  3. 不要在批处理中混合计数器与非计数器语句,计数器更新必须使用BEGIN COUNTER BATCH
  4. 控制批处理大小:默认 128 KiB 以上产生 WARN、1024 KiB 以上直接失败(多分区批处理)。若确有大批量写入需求,应分解为更小的批处理,并谨慎调整 conf/scylla.yaml 中的阈值——注释明确警告增大阈值可能导致节点不稳定。
  5. 强一致性表上的 BATCH 必须单分区,且 COUNTER 批处理不被支持;此时 LOGGED/UNLOGGED 的选择无关紧要,原子性由 Raft 保证。
  6. 不要将 BATCH 当作 SQL 事务使用:它没有完整的隔离语义,跨分区操作尤其如此;需要可序列化保证时,应结合唯一写入时间戳与轻量级事务(LWT)使用。

延伸阅读

  • docs/cql/dml/batch.rst:本文依据的官方 BATCH 参考文档
  • docs/cql/dml.rst:更新参数(TIMESTAMP/TTL/TIMEOUT)与更新排序、时间戳冲突解决机制的完整说明
  • docs/cql/index.rst:CQL 参考总目录
  • cql3/Cql.g:BATCH 的 ANTLR 文法
  • cql3/statements/batch_statement.cc:BATCH 的校验、mutation 生成与执行逻辑
  • cql3/statements/strong_consistency/batch_statement.cc:强一致性表 BATCH 的 Raft 提交实现
  • db/batchlog_manager.cc:批处理日志的写入、重放与清理
  • conf/scylla.yaml:批处理大小阈值配置
  • test/cqlpy/test_batch.py:批处理大小阈值与错误处理的测试用例

【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询