You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DynamoDB创建时条件写入:非主键referenceId唯一校验问询

实现DynamoDB非主键属性的条件写入(唯一约束)

要实现仅当表中不存在指定referenceId的条目时才新建保存,而且referenceId不是主键(也不想设为主键),核心是解决两个问题:一是如何校验全局范围内的referenceId唯一性,二是避免并发场景下的竞态条件。下面给你两种可行的实现方案:

方案1:全局二级索引(GSI)+ 条件写入(低并发场景适用)

首先需要给你的DynamoDB表创建一个全局二级索引,将referenceId设为该索引的分区键(Partition Key)。这样我们可以快速查询是否存在对应referenceId的条目,再结合条件写入来减少竞态风险。

代码实现(Kotlin)

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBSaveExpression
import com.amazonaws.services.dynamodbv2.model.AttributeValue
import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException
import com.amazonaws.services.dynamodbv2.model.DynamoDBQueryExpression
import com.amazonaws.services.dynamodbv2.model.ExpectedAttributeValue

// 假设你的实体类是MyEntity
val entityToSave = MyEntity().apply {
    referenceId = "123"
    // 设置其他属性...
}

val referenceIdValue = "123"
val saveExpression = DynamoDBSaveExpression()

// 第一步:先查询GSI,检查是否存在该referenceId的条目
val queryExpression = DynamoDBQueryExpression<MyEntity>()
    .withIndexName("referenceId-index") // 你的GSI名称
    .withKeyConditionExpression("referenceId = :val")
    .withExpressionAttributeValues(mapOf(":val" to AttributeValue(referenceIdValue)))

val existingItems = dynamoDBMapper.query(MyEntity::class.java, queryExpression)

if (existingItems.isEmpty()) {
    // 第二步:如果不存在,执行条件写入(额外加一层保护,避免短时间内的并发写入)
    val expectedConditions = mutableMapOf<String, ExpectedAttributeValue>()
    // 条件:当前要写入的主键条目不存在,避免覆盖已有数据
    expectedConditions["id"] = ExpectedAttributeValue().withExists(false)
    saveExpression.setExpected(expectedConditions)
    
    try {
        dynamoDBMapper.save(entityToSave, saveExpression)
        println("条目保存成功")
    } catch (e: ConditionalCheckFailedException) {
        println("保存失败:可能存在并发写入,该referenceId已被占用")
    }
} else {
    println("保存失败:已存在referenceId为$referenceIdValue的条目")
}

说明

  • 必须先创建GSI:在DynamoDB控制台或通过API给表添加GSI,分区键设为referenceId,投影类型选ALL(或者按需选择需要的属性)。
  • 这种方案在低并发场景下足够用,但高并发时可能出现竞态(两个请求同时查询到空,然后都执行写入),所以如果是高并发场景,建议用方案2。

方案2:事务(TransactWriteItems)+ 唯一约束表(高并发场景适用)

为了彻底避免竞态条件,我们可以创建一个专门的唯一约束表(比如ReferenceIdUnique),将referenceId作为该表的主键。然后在事务中先尝试写入这个约束表(条件是该主键不存在),如果成功,再写入主表。这样整个操作是原子性的,确保唯一性。

代码实现(Kotlin)

import com.amazonaws.services.dynamodbv2.model.*
import java.util.UUID

val referenceIdValue = "123"
val mainTableName = "YourMainTable"
val uniqueConstraintTableName = "ReferenceIdUnique"

// 构建事务请求
val transactItems = mutableListOf<TransactWriteItem>()

// 1. 尝试写入唯一约束表:仅当referenceId不存在时才写入
val putUniqueItem = PutItemRequest()
    .withTableName(uniqueConstraintTableName)
    .withItem(mapOf(
        "referenceId" to AttributeValue(referenceIdValue),
        "timestamp" to AttributeValue(System.currentTimeMillis().toString())
    ))
    .withConditionExpression("attribute_not_exists(referenceId)")

transactItems.add(TransactWriteItem().withPut(putUniqueItem))

// 2. 写入主表的请求
val putMainItem = PutItemRequest()
    .withTableName(mainTableName)
    .withItem(mapOf(
        "id" to AttributeValue(UUID.randomUUID().toString()), // 你的主表主键
        "referenceId" to AttributeValue(referenceIdValue),
        // 其他属性...
    ))

transactItems.add(TransactWriteItem().withPut(putMainItem))

// 执行事务
try {
    amazonDynamoDB.transactWriteItems(TransactWriteItemsRequest().withTransactItems(transactItems))
    println("条目保存成功")
} catch (e: TransactionCanceledException) {
    println("保存失败:referenceId为$referenceIdValue的条目已存在")
} catch (e: Exception) {
    println("保存失败:${e.message}")
}

说明

  • 唯一约束表的结构很简单:主键是referenceId,可以额外加一个timestamp字段用于清理过期数据(如果需要)。
  • 事务操作是原子性的:要么两个写入都成功,要么都失败,彻底避免了竞态条件,适合高并发场景。

补充:关于你给出的代码片段

你之前的代码片段提到了DynamoDBSaveExpression和ExpectedAttribute,如果只是用ExpectedAttribute设置attribute_not_exists(referenceId),那只能检查当前要写入的条目(基于主键)是否包含referenceId属性,而不是全局范围内是否存在该值的条目,这和你的需求不符,所以一定要结合GSI或者事务来实现全局唯一性。

内容的提问来源于stack exchange,提问作者ses

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 08:31:47