具有多个条件的 DynamoDB 事务插入(PK/SK attribute_not_exists 和 SK attribute_exists)

DynamoDB transactional insert with multiple conditions (PK/SK attribute_not_exists and SK attribute_exists)

我有一个带有 PK(字符串)和 SK(整数)的 table - 例如

PK_id                   SK_version      Data
-------------------------------------------------------
c3d4cfc8-8985-4e5...    1               First version
c3d4cfc8-8985-4e5...    2               Second version

我可以进行条件插入以确保我们不会使用 ConditionalExpression(在 GoLang SDK 中)覆盖 PK/SK 对:

putWriteItem := dynamodb.Put{
    TableName:                           "example_table",
    Item:                                itemMap,
    ConditionExpression:                 aws.String("attribute_not_exists(PK_id) AND attribute_not_exists(SK_version)"),
}

不过我也想确保SK_version总是连续的但不知道如何写表达式。在伪代码中是:

putWriteItem := dynamodb.Put{
    TableName:                           "example_table",
    Item:                                itemMap,
    ConditionExpression:                 aws.String("attribute_not_exists(PK_id) AND attribute_not_exists(SK_version) **AND attribute_exists(SK_version = :SK_prev_version)**"),
}

有人可以告诉我如何写这个吗?

在 SQL 我会做这样的事情:

INSERT INTO example_table (PK_id, SK_version, Data)
SELECT {pk}, {sk}, {data}
WHERE NOT EXISTS (
                    SELECT  1 
                    FROM    example_table
                    WHERE   PK_id = {pk}
                       AND  SK_version = {sk}
                 )
   AND EXISTS    (
                    SELECT  1
                    FROM    example_table
                    WHERE   PK_id = {pk}
                       AND  SK_version = {sk} - 1
                 )

谢谢

您正在思考 SQL 和非 SQL 数据库之间的一些差异。当然,DynamoDB 是一个没有 SQL 的数据库。它不支持开箱即用的乐观锁定。我看到两个直接的选项:

  1. 使用软件层锁定 DynamoDB table。这可能可行也可能不可行,具体取决于对 table 进行更新的频率。 'versions' 的生成速度以及您的应用程序可以锁定的最长时间可能会告诉您这是否适合您。我不熟悉围棋,但是JavaAPIsupports this。同样,这不是 DynamoDB 的内置功能。如果没有这样的 Go API 等价物,您可以使用 link 到 'lock' 和 table 中描述的技术进行更新。一般来说,锁定一个 no-SQL 数据库不是典型的模式,因为它并不是创建它的目的(其中一部分是在非结构化文档上实现大规模以允许快速访问许多消费者一次)

  2. 停止使用递增器来保证唯一性。通常,增量器在 DynamoDB 中不受欢迎,部分原因是缺乏对它的内在支持,部分原因是 DynamoDB 分片的方式不希望记录之间有太多相似性。使用 UUID 将解决唯一性问题,但如果您正在移植现有应用程序,则意味着对创建该 ID 的元素进行更多更改并更新读取 ID(可能包括一个创建时间字段,以便您可以分辨哪个是最新的,或者在 UUID 前添加或附加一个纪元时间来做同样的事情)。 一个解释为什么使用 UUID 而不是递增整数的 SO 问题。

条件检查应用于单个项目。它不能跨越多个项目。换句话说,您只需要多个条件检查。 DynamoDb 有 transactWriteItems API 和 writes/deletes 一起执行多个条件检查。下面的代码在 nodejs.

  const previousVersionCheck = {
    TableName: 'example_table',
    Key: {
      PK_id: 'prev_pk_id',
      SK_version: 'prev_sk_version'
    },
    ConditionExpression: 'attribute_exists(PK_id)'
  }

  const newVersionPut = {
    TableName: 'example_table',
    Item: {
      // your item data
    },
    ConditionExpression: 'attribute_not_exists(PK_id)'
  }

  await documentClient.transactWrite({
    TransactItems: [
      { ConditionCheck: previousVersionCheck },
      { Put: newVersionPut }
    ]
  }).promise()

事务有2个操作:一个是对之前版本的验证,另一个是条件写入。他们的任何条件检查失败,交易失败。

根据 Hung Tran 的回答,这里是一个 Go 示例:

checkItem := dynamodb.TransactWriteItem{
    ConditionCheck: &dynamodb.ConditionCheck{
        TableName:           "example_table",
        ConditionExpression: aws.String("attribute_exists(pk_id) AND attribute_exists(version)"),
        Key:                 map[string]*dynamodb.AttributeValue{"pk_id": {S: id}, "version": {N: prevVer}},
    },
}
putItem := dynamodb.TransactWriteItem{
    Put: &dynamodb.Put{
        TableName:           "example_table",
        ConditionExpression: aws.String("attribute_not_exists(pk_id) AND attribute_not_exists(version)"),
        Item:                data,
    },
}
writeItems := []*dynamodb.TransactWriteItem{&checkItem, &putItem}

_, _ = db.TransactWriteItems(&dynamodb.TransactWriteItemsInput{TransactItems: writeItems})