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
相关产品推荐
相关产品推荐

