Spanner DML 事务取消累计 Mutation 上限:大事务如何安全落地

2026-09-10 32 预计阅读时间: 1 分钟
来源: cloud.google.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

Google Cloud Spanner 现在为 DML 事务提供了更灵活的 Mutation 限制模型。过去,一个事务中所有 DML 语句产生的 mutation mods 会累计计算,并受到 80,000 的事务级上限约束;现在,这个上限改为逐条 DML 语句计算。

这意味着应用可以在一个 ACID 事务中组织更多 INSERTUPDATEDELETE 操作,而不必为了绕过累计上限,过早拆分业务流程。不过,单条 DML 语句的 80,000 mods 限制、事务大小限制,以及锁竞争风险并没有消失。

从“事务累计限制”变成“语句独立限制”

此前,如果一个事务包含多条 DML 语句,所有语句产生的 mutation mods 会加总。例如:

UPDATE Products ...   -> 30,000 mods
UPDATE Orders ...     -> 25,000 mods
INSERT OrderItems ... -> 30,000 mods
---------------------------------------
事务累计             -> 85,000 mods,超过限制

即使每条语句单独都没有超过 80,000 mods,整个事务仍可能失败。

现在,Spanner 会分别检查每一条 DML 语句:

UPDATE Products ...   -> 30,000 mods,允许
UPDATE Orders ...     -> 25,000 mods,允许
INSERT OrderItems ... -> 30,000 mods,允许

只要每条 DML 语句产生的 mods 少于 80,000,多个 DML 语句就可以继续放在同一个事务中。这样,应用可以按照业务原子性来划分事务,而不是围绕一个累计 mutation 上限被迫拆分。

这里的 mods 并不只是“影响了多少行”。它通常还会考虑被修改的单元格、主键,以及相关二级索引产生的更新。因此,更新一行包含多个索引的表,实际 mutation 成本可能明显高于直觉中的一行。

一个典型场景:订单、库存和审计记录一起提交

电商结算过程经常需要同时完成多类修改:更新商品库存、写入订单明细、更新时间戳,甚至记录风控结果。只要这些变化必须同时成功或同时回滚,就应该考虑使用同一个读写事务。

下面的 Java 示例使用 Spanner 客户端的 DML API,在同一事务中执行多条语句。示例假设 dbClient 已经完成初始化,并且数据库连接配置已经准备好。

import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TransactionContext;
import com.google.cloud.spanner.TransactionRunner.Work;

public class CheckoutService {
  private final DatabaseClient dbClient;

  public CheckoutService(DatabaseClient dbClient) {
    this.dbClient = dbClient;
  }

  public void completeCheckout(long orderId, long productId, int quantity) {
    dbClient
        .readWriteTransaction()
        .run(
            new Work<Void>() {
              @Override
              public Void doWork(TransactionContext transaction) {
                Statement reserveProduct =
                    Statement.newBuilder(
                            "UPDATE Products "
                                + "SET InStock = FALSE "
                                + "WHERE ProductId = @productId")
                        .bind("productId")
                        .to(productId)
                        .build();
                transaction.executeUpdate(reserveProduct);

                Statement insertItem =
                    Statement.newBuilder(
                            "INSERT INTO OrderItems "
                                + "(OrderId, ItemId, Quantity) "
                                + "VALUES (@orderId, @itemId, @quantity)")
                        .bind("orderId")
                        .to(orderId)
                        .bind("itemId")
                        .to(productId)
                        .bind("quantity")
                        .to(quantity)
                        .build();
                transaction.executeUpdate(insertItem);

                Statement updateOrder =
                    Statement.newBuilder(
                            "UPDATE Orders "
                                + "SET LastUpdated = PENDING_COMMIT_TIMESTAMP() "
                                + "WHERE OrderId = @orderId")
                        .bind("orderId")
                        .to(orderId)
                        .build();
                transaction.executeUpdate(updateOrder);

                return null;
              }
            });
  }
}

在新的限制逻辑下,示例中的每次 executeUpdate 都会独立对照 80,000 mods 上限。三条语句产生的 mods 仍会影响事务整体资源消耗,但不会因为 DML 语句之间的累计值超过 80,000 而直接触发原先的事务级 DML mutation 限制。

这并不意味着可以无限扩大事务。每条语句仍不能独自产生超过 80,000 mods,事务的字节大小等其他限制也仍然有效。

DML 和 Mutation API 不能混为一谈

这次调整针对的是 DML 语句,不是所有写入方式。

DML 语句

使用 executeUpdate 执行 INSERTUPDATEDELETE 时,每条语句单独接受 80,000 mods 检查。多个 DML 语句可以放在同一个事务中。

适合以下情况:

  • 修改逻辑可以用 SQL 清晰表达;
  • 需要在一个事务中组合多个业务步骤;
  • 希望让 Spanner 根据查询条件执行更新,而不是在客户端构造大量行级 mutation。

Mutation API

如果应用使用客户端库中的 insert()update() 等 Mutation API,mutation 通常会在 Commit 调用时一次性提交。此时,80,000 mods 限制仍然适用于该次提交中包含的整组 mutations。

因此,不能简单地把下面两种写法视为等价:

多个 executeUpdate() 调用
-> 每条 DML 独立检查

一次 Commit(mutations)
-> 整个 mutation 集合一起检查

如果现有应用已经使用 Mutation API,并且单次提交经常接近上限,那么这次 DML 限制调整不会自动解决问题。可以评估是否将部分逻辑改写为 DML,或者把批量写入拆成多个提交批次。

先测量,再扩大事务边界

事务扩大后,最容易被忽视的问题不是 mutation 数量,而是锁的持有时间。更大的事务通常意味着:

  • 锁持续时间更长;
  • 并发请求更容易发生锁竞争;
  • 事务重试和 abort 的概率可能上升;
  • 单次失败需要重做的工作更多。

建议在生产环境关注提交统计信息中的 mutation_count,了解一个已提交事务实际产生了多少 mutation。这个统计值会包含事务中所有 DML 语句以及其他 commit 操作产生的 mutations。

可以把事务设计成“业务上完整,但技术上简洁”:

  1. 只在事务中执行必须具备原子性的写入;
  2. 不要把远程 HTTP 请求、文件操作或长时间计算放在事务回调中;
  3. 对可能影响大量行的单条 DML 先估算行数、列数和索引数量;
  4. 观察锁等待、abort 和重试情况,再决定是否继续扩大事务;
  5. 为事务重试设计幂等逻辑,避免应用层副作用被重复执行。

单条批量操作超过上限怎么办

这次更新没有取消单条 DML 的 80,000 mods 限制。如果一条语句本身就会更新过多数据,例如:

UPDATE Products
SET InStock = FALSE
WHERE Category = 'seasonal-clearance';

并且该条件命中了大量行,那么它仍然可能失败。此时可以根据业务需求选择不同方案:

  • 使用更窄的主键范围分页执行;
  • 将一次大更新拆成多个短事务;
  • 对不要求单一事务原子性的后台批处理使用 Partitioned DML;
  • 减少不必要的更新列和索引写入;
  • 重新评估这次操作是否真的需要与其他业务写入保持同一事务。

例如,按主键范围分页的思路可以这样实现。下面是可改造的伪代码,具体参数绑定方式取决于所使用的 Spanner 客户端库版本:

long pageSize = 1000;
long startProductId = 0;

while (true) {
  Statement statement =
      Statement.newBuilder(
              "UPDATE Products "
                  + "SET InStock = FALSE "
                  + "WHERE ProductId > @startId "
                  + "AND ProductId <= @endId")
          .bind("startId")
          .to(startProductId)
          .bind("endId")
          .to(startProductId + pageSize)
          .build();

  // 每个批次使用一个较短的读写事务执行。
  // 提交成功后再推进 startProductId。
  // transaction.executeUpdate(statement);

  startProductId += pageSize;
  // 实际代码还需要根据查询结果判断是否已经处理完毕。
}

分页不是无条件的最佳选择:如果业务要求所有行必须与订单状态变化严格原子提交,就不能用多个独立事务替代原来的事务。此时应优先优化单条语句范围,或者重新设计数据模型和业务边界。

上线前的检查清单

  • 确认应用使用的是 DML 还是 Mutation API;两者的 80,000 mods 计算方式不同。
  • 检查每条 DML 语句,而不是只看事务中修改的总行数。
  • 把主键、修改列和二级索引纳入 mutation 成本估算。
  • 通过 CommitStats 记录并分析 mutation_count
  • 压测锁竞争、事务 abort 和自动重试,而不是只验证提交成功率。
  • 保留单条语句超过 80,000 mods 时的分页或 Partitioned DML 方案。
  • 不要因为上限放宽,就把不相关的业务操作全部塞进一个长事务。

这项变化的核心价值,是让 Spanner 事务边界更多由业务一致性决定,而不是被 DML 的累计 mutation 上限牵着走。合理使用时,应用可以在保持 ACID 语义的同时组合更复杂的实时更新;但真正的工程目标仍然是短事务、可观测、可重试,并且让每条大规模 DML 都有明确的容量边界。


相关推荐