在 Google Spanner 中插入(而不是更新)时的自动 created_on(不是 updated_on)时间戳

Automatic created_on (not updated_on) timestamp on insert (not update) in Google Spanner

据我所知,没有安全的方法(在并发环境中)执行此操作,但我想确保我没有遗漏任何内容。

通常,在我们的数据库中,我们喜欢跟踪一行最初创建的时间和最后更新的时间。分别地。这不是 "created_at" 列,实际上应该称为 "updated_on"。

在 Spanner 中,使用提交时间戳(或者甚至只是始终输入当前时间),updated_on 很容易。但是,我使用的常用工具 created_on:

好像没有。我想,也许你可以设置一个云函数,这看起来有点矫枉过正(具有讽刺意味的是,云函数会矫枉过正......)。

我能想到的最接近但并不奇怪的事情是尝试插入突变,捕获异常,检查 ErrorCode.ALREADY_EXISTS,然后更新。并且只在插入块中设置 created_on 。丑陋的......而且在面对并发删除时也不是很安全(你插入,你发现错误,中间有人删除,尝试更新,繁荣)

还有其他建议吗?最好通过 SDK?

我可以想到两种可能的解决方案:

  1. 您可以添加两列,一列用于 created_at,一列用于 updated_on。插入行时,将 created_at 和 updated_on 设置为 spanner.commit_timestamp() 占位符。更新行时,只需将 updated_on 更改为 spanner.commit_timestamp()。

  2. 创建一个事务来封装突变。在单笔交易中,您可以:

    • 读取 table 以检查该行是否存在
    • 如果该行已存在,则更新该行
    • 如果该行不存在,则插入该行

如果您在单个事务中执行这些操作,您将避免您提到的竞争条件,因为事务是隔离的。

可以在此处找到有关提交时间戳的更多信息:https://cloud.google.com/spanner/docs/commit-timestamp

请注意,正如我在评论中所阐明的那样,我正在寻找干净的东西,而不是交易,因为交易的工作方式有点难看(接口至少应该有 lambdas 作为一个选项,而不是匿名 类 :/.) 或者更好,只是 beginTransaction()、endTransaction(),例如:

            this.dbClient.readWriteTransaction()
            .run(
                new TransactionCallable<Void>() {
                    @Nullable
                    @Override
                    public Void run(TransactionContext transactionContext) throws Exception {
                        Struct row = transactionContext.readRow(
                            MY_TABLE,
                            Key.of(
                                keyCol1,
                                keyCol2
                            ),
                            Collections.singletonList(keyCol1)
                        );
                        //this creates a write builder with a bunch of columns
                        //set to mutated, except for CREATED_ON
                        WriteBuilder writeBuilder = updateBuilder(
                            Mutation.newInsertBuilder(MY_TABLE),
                            myDataModel
                        );
                        if(row == null) {
                            writeBuilder.set(CREATED_ON).to(Timestamp.now()).build();
                        }
                        Mutation recMut =
                            updateBuilder(Mutation.newUpdateBuilder(MY_TABLE), myDataModel).build();
                        transactionContext.buffer(recMut);
                        return null;
                    }
                }
            );

@RedPandaCurious 是正确的,Scott 的回答只成功了一半:(1) 注定会失败,原因在问题中列出 - 或者采取另一种方式,只是重新说明我想要完成的事情,而没有说明(2) 如何只是重述我的后续评论,而没有提供任何更多细节或文档。

@RedPandaCurious,如果你想注意到事务比捕获异常更快,有一些相关的文档(我特别好奇它们是否更快,总体而言,对于各种工作负载,面对许多并发操作,对于处理异常的一个客户端来说不一定更快),这作为一个答案是有意义的。不过,最终,交易是最正确、最理智的推理方式。这就是为什么我最终采用这种方法的原因 - 因为任何一种方法都很丑陋。

好吧,事实证明,如果你删除 @Nullable 注释,你可以使用 lambdas,并且通过一些额外的重构,将其减少到:

 /**
 * Lambda interface for factoring out transactional execution logic
 */
 public interface SpannerOperation {
      Boolean doOperation(TransactionContext ctxt, Struct row);
  }

  private Boolean executeIfExists(
        ... key fields ...
        SpannerOperation spannerOperationIfExists,
        SpannerOperation spannerOperationifNotExists,
        Iterable<String> columns
    ) {
        return this.dbClient.readWriteTransaction().run(
            transactionContext -> {
                Struct row = transactionContext.readRow(
                    MY_TABLE,
                    Key.of(...), //you could even pass the key in as a key
                    columns
                );
                if(row != null) {
                    spannerOperation.doOperation(transactionContext, row);
                    return true;
                } else {
                   spannerOperationifNotExists.doOperation(transactionContext, null);
                    return false;
                }
            }
        );
    }

     public boolean doSomething(... keyStuff.. )
        return this.executeIfExists(
            .. key fields ...
            (ctxt, row) -> {
                Mutation mut = Mutation
                    .newUpdateBuilder(MY_TABLE)
                    .....//as you like it...
                    .build()
                ctxt.buffer(mut);
                return true;
            },
            (ctxt, row) -> false, //set created_on or whatever
            Collections.singleton(..some column you want to read in..)
        );

请注意,这也适用于附加到列表等内容,并且所有这些都被分解为您需要的内容。 Google 确实需要一个 ifExists() 方法 - 我最终在很多地方使用了它..;)