FEATURED · 精选文章

ScyllaDB LWT 轻量事务实战:从 IF 子句语义到 Paxos 实现与 Paxos 状态表

发布时间 / 2026/9/14 19:02:19
来源 / 创域科博编辑部
栏目 / 资讯中心
ScyllaDB LWT 轻量事务实战:从 IF 子句语义到 Paxos 实现与 Paxos 状态表 ScyllaDB LWT 轻量事务实战从 IF 子句语义到 Paxos 实现与 Paxos 状态表【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladbScyllaDB 的轻量事务Lightweight TransactionsLWT为仅在数据满足特定条件时才修改的场景提供了线性一致的语义保障。本文基于官方特性文档 docs/features/lwt.rst 系统讲解 LWT 的条件语句语法、Paxos 共识写入路径、$paxos状态表机制与错误处理策略并结合 service/paxos 目录下的源码印证底层协议各轮次的真实实现读完后可掌握 LWT 的完整用法、原理与生产环境中的注意事项。一、为什么需要 LWT在某些场景中必须基于数据的当前状态来决定是否修改数据例如只有当某行不存在时才插入或只有当某列等于特定值时才更新。LWT 正是为此而生——它只允许在条件表达式求值为真时才对数据执行变更。条件语句提供线性一致性linearizable语义从而保证数据的一致性。基本经验法则任何带有 IF 子句的语句都是条件语句含有至少一条条件语句的 BATCH 则是条件批处理conditional batch。条件语句和条件批处理作为轻量事务原子地执行。例如UPDATE employees SET join_date 2018-05-19 WHERE firstname John AND lastname Doe IF join_date ! null;上面的例子仅当employees表中存在 John Doe 的记录时才更新其入职日期。二、IF 子句的语义通过 IF 子句构造条件语句时可以查询单个单元格cell和/或集合元素可以使用 IN 子句也可以使用比较运算符、、、、、!。要点要启动事务条件必须求值为 true若条件中包含多个不同元素则每一部分都必须求值为 true任何一部分求值为 false事务就不会完成。IF 条件可以由以下 CQL 组件之一构成组件说明IF EXISTS行存在时执行IF NOT EXISTS行不存在时执行对现有行的一个或多个谓词如IF col value、IF col ! null三、IF 子句作用于哪一行评估或赋值非静态列的条件语句必须同时指定聚簇键clustering key和分区键partition key——这类语句作用于普通行regular row而仅限定分区键的语句只能使用静态列作用于该分区的静态行static row。普通行存在的判定标准是至少一个普通单元格或聚簇键被赋值。以如下表为例CREATE TABLE t ( p INT, c INT, r INT, s INT STATIC, PRIMARY KEY(p, c));它有分区键p、聚簇键c、普通单元格r和静态单元格s。物化一个普通行只需给c或r中任意一个赋值 INSERT INTO t (p, c, r) VALUES (1,1,NULL) IF NOT EXISTS; ------------------------------------- | [applied] | p | c | s | r | |-------------------------------------| | True | null | null | null | null | ------------------------------------- INSERT INTO t (p, c, r) VALUES (1,1,NULL) IF NOT EXISTS; ----------------------------------- | [applied] | p | c | s | r | |-----------------------------------| | False | 1 | 1 | null | null | -----------------------------------第一次插入[applied] True行尚不存在第二次为False因为该行已被第一次的插入物化。物化一个静态行则必须显式给至少一个静态单元格赋值否则静态行不被视为存在 INSERT INTO t (p, s) VALUES (1,NULL) IF NOT EXISTS; ------------------------------------ | [applied] | p | c | s | r | |------------------------------------| | True | 1 | null | null | null | ------------------------------------| INSERT INTO t (p, s) VALUES (1,NULL) IF NOT EXISTS; ------------------------------------ | [applied] | p | c | s | r | |------------------------------------| | True | 1 | null | null | null | ------------------------------------注意两次均为True显式写入 NULL 的静态单元格并不使静态行存在。NULL 比较的语义条件中可以与NULL比较但由于 ScyllaDB 中 NULL 值与缺失值无法区分与NULL比较的条件在缺失行和含 NULL 单元格的现有行上会返回相同结果 UPDATE t SET s2 WHERE p1 IF s NULL; ------------------- | [applied] | s | |-------------------| | True | null | -------------------另一个值得注意的行为当普通行缺失、但静态单元格已赋值时静态单元格会出现在用于评估该缺失普通行条件的行中 UPDATE t SET r2 WHERE p1 AND c2 IF s 2; ------------------ | [applied] | s | |------------------| | True | 2 | ------------------四、ScyllaDB Paxos条件写入的底层路径带IF子句的语句走一条与普通写入不同的路径采用 Paxos 共识算法见上方Scylla Paxos Implementation流程图来保证执行历史的线性化。协议各轮次在源码中的对应关系非常清晰service/paxos/paxos_state.hh 中定义了每个副本 RPC 端点逐一对应协议轮次// Replica RPC endpoint for Paxos prepare phase. static futureprepare_response prepare(...); // Replica RPC endpoint for Paxos accept phase. static futurebool accept(...); // Replica RPC endpoint for Paxos learn. static future learn(...); // Replica RPC endpoint for pruning Paxos table static future prune(...);各阶段的工作机制如下Prepare/Promise 轮协调者首先创建一个唯一的时间型标识符ballot即 time-UUID发给各副本。副本收到 ballot 后回复承诺promise表示愿意接受与该 ballot 关联的变更。如果副本已经承诺过更新的 ballot它会拒绝当前 ballot——这一机制锁定了并发改写尝试使协调者得以不受干扰地读取并更新该行。协议状态持久化在每个副本本地的 Paxos 状态表见下文Paxos 状态表一节。源码中paxos_state结构恰好包含协议三要素_promised_ballot已承诺的 ballot、_accepted_proposal已接受的提案与_most_recent_commit最近一次提交。ScyllaDB 的独有优化与 Cassandra 不同ScyllaDB 在 Prepare 请求的响应中顺带捎带piggy-back行的旧版本因此读取行不需要额外的消息往返。源码注释 service/paxos/paxos_state.cc 中也体现了这一点prepare 阶段不读system.paxos之外的数据且读路径被合并进 prepare 轮。Accept 轮协调者获得多数派majority副本的承诺后评估IF条件若结果为真向副本发送更新后的变更mutation。副本将新行存入 Paxos 状态表并确认acknowledge接受。Quorum 交集规则一旦多数派副本接受了该行就满足quorum intersection规则——只要至少多数节点在线任何可能的新 quorum 都至少包含一个已接受上一版行因而知晓该变更的节点。Learn 轮协调者收到多数派 accept 后Paxos 协议轮结束此时可以安全地用新行更新基础表base table。Prune 轮基础表更新成功后协调者向客户端响应同时可以安全地从 Paxos 状态表中清理prune协议状态。清理在后台异步进行。一致性级别的配置quorum 的大小决定了协调者进入下一轮或向客户端响应之前必须获得多少确认Prepare 与 Accept 轮由SERIAL CONSISTENCY设置配置Learn 轮使用 ScyllaDB 最终一致的CONSISTENCYPrune 轮在后台执行。与 Cassandra 实现的两个关键差异从源码结构看ScyllaDB 的 Paxos 实现相对 Cassandra 有两个核心区别将 prepare 与 read 两个动作合并为一轮即上文提到的响应捎带旧行版本引入额外的异步 prune 轮使 Paxos 状态表保持小体积从而降低其被压缩compaction时的写放大。五、Paxos 状态表Paxos State TablesLWT 的 Paxos 协议状态存储于专门的内部表中具体使用哪张表取决于用户表基于 vnode 还是 tabletvnode 型用户表Paxos 状态存储在每个节点本地自动创建并维护的系统表system.paxos中该表持有该节点上所有 vnode 型用户表的 Paxos 状态。tablet 型用户表为每个用户表单独创建一张 Paxos 状态表创建发生在首次对该用户表执行 LWT 时表创建开销可能带来一次小的延迟尖峰。该表与用户表位于同一 keyspace命名遵循user-table$paxos模式。源码 service/paxos/paxos_state.cc 中定义了该后缀static const sstring paxos_state_table_suffix $paxos;。$paxos表复用与其用户表完全相同的 tablet 集合。这确保了 tablet 操作如迁移、split、merge会一致地同时作用于用户表的 tablet 和对应$paxos表的 tablet从而使 Paxos 状态在此类操作期间保持完整与一致。状态表的访问限制与生命周期对$paxos表的访问受到限制ScyllaDB 不对其授予任何显式权限仅 superuser 可以查看且即使是 superuser 也不能对其执行ALTER或DROP当用户表被删除时$paxos表会被自动删除。源码中 paxos_store::on_before_drop_column_family 钩子负责在基础表 drop 时一并宣告删除对应的状态表表的首次创建由 paxos_store::ensure_initialized 触发它检测到状态表不存在时调用create_paxos_state_table通过 group0Raft流程向集群宣告新表——这正是文档所说首次 LWT 引入创建开销的源码依据。状态表的列结构可以从 paxos_store::create_paxos_state_schema 读出分区键为row_key与原始行哈希到同一 token 的字节表示常规列包括promisetimeuuid、most_recent_commit冻结变更的二进制序列化、most_recent_commit_at、proposal和proposal_ballot——与第四节描述的协议状态一一对应。paxos_grace_seconds 与 TTL根据 docs/rst_include/note-ttl-lwt.rst 的说明system.paxos表中记录的 TTL 由paxos_grace_seconds值设定若未设置则回退使用gc_grace_seconds。两者的默认值相同均为 10 天但允许分别独立设置可通过修改system.paxos表来更改paxos_grace_seconds。源码 db/paxos_grace_seconds_extension.hh 中的注释进一步解释了该选项被刻意与gc_grace_seconds解耦的原因基础表可能采用完全不同的垃圾回收策略例如不使用删除因而可将gc_grace_seconds设为 0但 Paxos 表仍依赖 repair 达到一致性用户被要求必须在paxos_grace_seconds之内执行 repair。六、条件批处理Conditional BatchBATCH语句可以包含一条或多条条件语句这样的批处理称为条件批处理。整个条件批处理拥有数据库的隔离视图并按照全有或全无all-or-nothing原则执行。在许多方面条件批处理类似于关系数据库中的 ACID 事务——区别在于批处理仅当所有语句中的全部条件都为 true 时才执行否则什么都不做。七、使用 Paxos 读取对单分区查询可以使用 Paxos 进行读取。当你需要获取行的最新版本时Paxos 读非常有用普通的 QUORUM 读可能看不到当前正被条件写入更新的值。为确保读是序列化的请在 cqlsh 或预编译语句属性中将CONSISTENCY设为SERIALSELECT * FROM employees WHERE firstname John AND lastname Doe八、LWT 完整 CQL 实战示例电影排期场景以一家宣传电影院正在放映电影的公司为例应用允许客户检索电影并购买票。以下活动都可以用轻量事务实现通过新增放映时段来修改现有电影的上映信息不改变该电影的其他数据移除不再放映的电影向新片页添加一部新电影前提是它尚不存在。1. 创建 KeyspaceCREATE KEYSPACE movies WITH replication {class: NetworkTopologyStrategy, replication_factor : 3};2. 创建表CREATE TABLE movies.nowshowing ( movie TEXT, director TEXT static, main_actor TEXT static, released DATE static, location TEXT, run_day TEXT, run_time TIME, theater TEXT, PRIMARY KEY (movie, location, run_day, run_time) );该表主键为复合键(movie, location, run_day, run_time)而director、main_actor、released是静态列——正好同时覆盖了前文讲解的静态行与普通行两类 LWT 作用对象。3. 使用 LWT 向表中插入数据INSERT INTO movies.nowshowing (movie, director, main_actor, released) VALUES (Sonic the Hedgehog, Jeff Fowler, Ben Schwartz, 2020-14-02) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, director, main_actor, released) VALUES (Invisible Man, Leigh Whannell, Elisabeth Moss, 2020-28-02) IF NOT EXISTS;4. 查看当前表SELECT * FROM movies.nowshowing;movie | location | run_day | run_time | director | main_actor | released | theater ------------------------------------------------------------------------------------------------------ Sonic the Hedgehog | null | null | null | Jeff Fowler | Ben Schwartz | 2021-02-02 | null Invisible Man | null | null | null | Leigh Whannell | Elisabeth Moss | 2022-04-06 | null (2 rows)5. 向表中补充更多排期信息INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Sonic the Hedgehog, Times Square, AMC Empire 25, Saturday, 21:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Sonic the Hedgehog, Penn Station, AMC 34th Street 14, Sunday, 14:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Sonic the Hedgehog, Times Square, AMC Empire 25, Saturday, 14:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Sonic the Hedgehog, Penn Station, AMC 34th Street 14, Sunday, 21:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Invisible Man, Times Square, AMC Empire 25, Friday, 21:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Invisible Man, Penn Station, AMC 34th Street 14, Sunday, 22:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Invisible Man, Times Square, AMC Empire 25, Saturday, 22:00:00) IF NOT EXISTS; INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Invisible Man, Penn Station, AMC 34th Street 14, Sunday, 18:00:00) IF NOT EXISTS;6. 再次查看当前表SELECT * FROM movies.nowshowing;movie | location | run_day | run_time | director | main_actor | released | theater -------------------------------------------------------------------------------------------------------------------------------- Sonic the Hedgehog | Penn Station | Sunday | 14:00:00.000000000 | Jeff Fowler | Ben Schwartz | 2021-02-02 | AMC 34th Street 14 Sonic the Hedgehog | Penn Station | Sunday | 21:00:00.000000000 | Jeff Fowler | Ben Schwartz | 2021-02-02 | AMC 34th Street 14 Sonic the Hedgehog | Times Square | Saturday | 14:00:00.000000000 | Jeff Fowler | Ben Schwartz | 2021-02-02 | AMC Empire 25 Sonic the Hedgehog | Times Square | Saturday | 21:00:00.000000000 | Jeff Fowler | Ben Schwartz | 2021-02-02 | AMC Empire 25 Invisible Man | Penn Station | Sunday | 18:00:00.000000000 | Leigh Whannell | Elisabeth Moss | 2022-04-06 | AMC 34th Street 14 Invisible Man | Penn Station | Sunday | 22:00:00.000000000 | Leigh Whannell | Elisabeth Moss | 2022-04-06 | AMC 34th Street 14 Invisible Man | Times Square | Friday | 21:00:00.000000000 | Leigh Whannell | Elisabeth Moss | 2022-04-06 | AMC Empire 25 Invisible Man | Times Square | Saturday | 22:00:00.000000000 | Leigh Whannell | Elisabeth Moss | 2022-04-06 | AMC Empire 25 (8 rows)九、LWT 更新中的算术 SETScyllaDB 扩展ScyllaDB 允许在条件UPDATE中对非 counter 数值列使用SET col col value与SET col col - value。由于语句作为 LWT 执行读-改-写是原子的不需要单独的读也不存在竞态条件。此处必须有 IF 条件否则 ScyllaDB 会拒绝该语句。若该列为 null则算术结果也为 null标准 SQL 的 null 传播语义该列保持未设置。要捕获列未初始化的情况请直接使用检查该列的IF条件如IF r ! null而不是IF EXISTS-- 初始化行数值常规列 r 置 0 INSERT INTO mytable (pk, ck, r) VALUES (1, 1, 0) IF NOT EXISTS; -- 原子自增 r UPDATE mytable SET r r 1 WHERE pk 1 AND ck 1 IF EXISTS; -- 原子自增 r——若 r 未初始化则显式失败 UPDATE mytable SET r r 1 WHERE pk 1 AND ck 1 IF r ! null; -- 仅当值足够大时将 r 原子减 5 UPDATE mytable SET r r - 5 WHERE pk 1 AND ck 1 IF r 5;注意该语法是 ScyllaDB 扩展Apache Cassandra 不支持。十、使用 LWT 更新表假设 Times Square 的 AMC 影院需要将影院名从 AMC Empire 25 改为 AMC Empire可以用条件 UPDATE 实现UPDATE movies.nowshowing SET theater AMC Empire WHERE location Times Square AND run_day Saturday AND run_time 14:00:00 AND Movie Sonic the Hedgehog IF EXISTS;返回结果集首列[applied] True表示更新已生效结果中同时带回主键列与条件表达式所用列的旧值[applied] | movie | location | run_day | run_time | director | main_actor | released | theater --------------------------------------------------------------------------------------------------------------------------------- True | Sonic the Hedgehog | Times Square | Saturday | 14:00:00.000000000 | Jeff Fowler | Ben Schwartz | 2021-02-02 | AMC Empire 25再次SELECT * FROM movies.nowshowing;可确认第 3 行的影院名已变为 AMC Empire注意仅目标行被更新其他行不受影响。十一、使用条件批处理更新表如果想像更新某地电影的放映时间由于run_time是主键的一部分无法直接修改。但可以删除后重新插入记录并用批处理保证原子性BEGIN BATCH DELETE FROM movies.nowshowing WHERE movie Sonic the Hedgehog AND location Times Square AND run_day Saturday AND run_time 21:00:00 IF EXISTS INSERT INTO movies.nowshowing (movie, location, theater, run_day, run_time) VALUES (Sonic the Hedgehog, Times Square, AMC Empire 25, Saturday, 23:00:00) APPLY BATCH;DELETE ... IF EXISTS使整个批处理成为条件批处理只有旧记录确实被删除时后续插入才会执行。十二、使用条件批处理删除分区电影 Invisible Man 停映后影院希望将其整体删除。批处理需要将批处理标记为条件IF EXISTS使其原子化删除整个分区。BEGIN BATCH UPDATE movies.nowshowing SET released NULL WHERE movie Invisible Man IF EXISTS DELETE FROM movies.nowshowing WHERE movie Invisible Man APPLY BATCH;先条件更新清除静态列released再删除整个分区任一步骤条件不满足整个批处理不做任何事。十三、IF 与 WHERE 的区别有人会想 IF 子句是不是应该取代 WHERE——在很大程度上确实如此两者都接受表达式且都作用于被检索的行。但关键差异在于IF 条件从不使用二级索引因为行先被取出然后条件才求值IF 条件仅作用于完全限定的行fully qualified row即必须指定分区键多数情况下还必须指定聚簇键在 WHERE 子句中会配合DELETE/UPDATE/SET使用用UPDATE和INSERT配WHERE时还需要VALUES子句如果数据选择取回多行IF条件不能产生歧义——即不能对某一行求值为 TRUE 而对另一行求值为 FALSE。这包括仅限定分区键而未限定聚簇键的语句或分区键 多个聚簇键的情形pk ? AND ck IN (?, ?, ?)。在这些情形下只有针对静态单元格的条件才被接受。十四、处理条件语句的结果集条件语句总是返回一个结果集。结果集的第一列[applied]是特殊列指示语句是否被应用紧随其后的依次是主键列以及条件表达式中使用到的列——结果集中包含这些列的先前旧值。若条件是IF EXISTS或IF NOT EXISTS则列族的所有列都成为结果集的一部分如果语句插入了新行先前值不存在结果中将包含 NULL。批处理可包含多条条件语句。批中每条条件语句各产生结果集的一行按语句顺序返回。非条件语句可以存在于条件批处理中但不贡献结果集行。条件批处理结果集的列集合是批内所有条件语句的条件表达式所用列的并集。例如BEGIN BATCH UPDATE movies.nowshowing SET main_actor Aldis Hodge WHERE movie Invisible Man IF main_actor Elisabeth Moss UPDATE movies.nowshowing SET director Mr Saw WHERE movie Invisible Man IF director Leigh Whannell APPLY BATCH;[applied] | movie | location | run_day | run_time | director | main_actor --------------------------------------------------------------------------------------- True | Invisible Man | null | null | null | Leigh Whannell | Elisabeth Moss True | Invisible Man | null | null | null | Leigh Whannell | Elisabeth Moss十五、错误处理条件语句LWT执行出错并不必然意味着它失败了。除了客户端超时、服务器过载、内存分配失败等常规失败原因外Paxos 协议本身存在一些导致不确定性的场景一个节点发起的更新允许被另一个节点赶超并完成overtaken协调者可能在写入已在多数参与者处成功即事实上已提交之后失败或超时。要确知结果客户端必须把值读回或重试该操作直到成功。重试只应在失败是瞬态transient时进行——例如语法错误时重试毫无意义。主要瞬态错误如下错误说明Unavailable exception协调者无法联系上语句一致性级别所要求的副本数量。可能因一个或多个节点宕机也可能环上根本没有足够的副本满足 keyspace 的复制因子。协调者会针对 Paxos 的 COMMIT 和 LEARN 两个步骤分别独立做可用性检查以满足SERIAL CONSISTENCY与CONSISTENCY设置。除非集群配置有误否则属于瞬态失败。可通过返回的一致性级别判断失败发生在哪一步若 CL 为SERIAL或LOCAL_SERIAL失败发生在语句尚可能产生任何影响之前而发生在 LEARN 阶段的失败则意味着语句已经由 Paxos 提交此时返回的 CL 为ONE、QUORUM或类似级别。Write timeout可发生在 Paxos 的任何步骤。若发生在多数节点已收到写入之后尽管失败报错该写入实际上已提交。Read timeout含义同 Write timeout但用于 SELECT。内部地看条件更新可能执行读操作但这些读期间的超时仍以上报为 write timeout。十六、限制Limitations以下限制必须遵守不能用条件批处理修改多个分区不能对使用 counter 的表使用 LWT使用用户提供的 timestamp 时必须确保 timestamp 由事务协调者分配否则无法保证一致性不建议将条件LWT语句与非条件语句作用于同一数据集因为此时无法保证条件语句的一致性。简言之所有 LWT 数据集都不应使用非 LWT 形式的写操作。更次要的限制非 LWT 批处理可以是 UNLOGGED但条件批处理不行IF 条件必须是完美的合取... AND ... AND ...条件批处理总会记录在 Paxos 状态表中因此对它们UNLOGGED关键字会被静默忽略。十七、延伸阅读仓库知识库中还有两篇相关文档建议配合本文阅读How does ScyllaDB LWT Differ from Apache Cassandra?docs/kb/lwt-differences.rst——详解 ScyllaDB 轻量事务实现与 Apache Cassandra 的差异与本文第四节两个关键差异互为补充How to Change gc_grace_seconds for a Tabledocs/kb/gc-grace-seconds/——讲解如何修改gc_grace_seconds参数与第五节的paxos_grace_seconds配置相关联。小结LWT 是 ScyllaDB 中实现读-改-写原子性的核心机制。掌握 IF 子句的行作用域普通行 vs 静态行、[applied]结果集语义、SERIAL/SERIAL CONSISTENCY一致性级别的选择、$paxos状态表的自动生命周期以及 Paxos 失败的不确定性与重试策略是在生产环境中正确使用轻量事务的完整拼图。【免费下载链接】scylladbNoSQL data store using the Seastar framework, compatible with Apache Cassandra and Amazon DynamoDB项目地址: https://gitcode.com/GitHub_Trending/sc/scylladb创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻