DynamoDB的Condition Expression(条件表达式)是写操作中用来做前置校验的机制,它决定了这次PutItem、UpdateItem、DeleteItem甚至BatchWriteItem是否真正生效。很多团队在没有理解它的语义前就直接上线,结果出现数据被覆盖、重复插入、并发更新互相冲突等问题。这篇文章围绕条件表达式的语法、典型用法和常见报错展开,帮助你把这个特性用对、用透。

条件表达式的基本语法与常用函数
条件表达式的核心思想很简单:只有当表达式求值结果为true时,这次写操作才会执行,否则DynamoDB会抛出ConditionalCheckFailedException异常并回滚整个操作。它类似于SQL里的WHERE子句,但作用对象是即将被写入的那条数据,而不是批量筛选。
最常用的两个函数是attribute_exists和attribute_not_exists。其中attribute_not_exists(partitionKey)是实现幂等写入的经典写法:只有当主键不存在时才允许插入,如果两个请求并发插入同一个主键,只会有一个成功。下面用AWS CLI演示一下:
# 只有当id为user-1001的记录不存在时才插入,防止重复创建
aws dynamodb put-item \
--table-name Users \
--item '{"id": {"S": "user-1001"}, "name": {"S": "Alice"}}' \
--condition-expression "attribute_not_exists(id)"
除了存在性判断,条件表达式还支持比较运算符(=、<>、<、<=、>、>=)、逻辑组合(AND、OR、NOT)以及几个便捷函数:begins_with、contains、attribute_type、size。需要注意的是,条件表达式中的操作对象只能是当前这条item的属性,不能跨item查询,这一点和SQL的WHERE有本质区别。
另外,条件表达式不能引用保留字作为属性名,比如name、status、count都是DynamoDB的保留字。如果直接写condition-expression "status = :s"会直接报验证错误,必须使用表达式属性名#s来替代。这是一个非常高频的坑,后面单独说。
带参数的写法与占位符规范
和UpdateExpression一样,条件表达式强烈建议使用占位符::value代表属性值,#name代表属性名。直接把值内联写进表达式虽然在小脚本里没问题,但在应用程序里容易引发类型错误和注入风险。以Java SDK v2为例:
Map<String, AttributeValue> item = new HashMap<>();
item.put("id", AttributeValue.builder().s("user-1001").build());
item.put("version", AttributeValue.builder().n("3").build());
Map<String, AttributeValue> exprValues = new HashMap<>();
exprValues.put(":v", AttributeValue.builder().n("3").build());
PutItemRequest request = PutItemRequest.builder()
.tableName("Users")
.item(item)
.conditionExpression("version = :v") // 只有版本号匹配才覆盖
.expressionAttributeValues(exprValues)
.build();
上面的例子其实就是乐观锁的雏形:每个客户端写入时都带上自己读到的版本号,服务端校验版本一致才允许覆盖。如果版本不匹配,说明别人已经改过这条数据,当前请求需要重新读取后再试。比起依赖DynamoDB事务,这种方案开销小、实现简单,是并发更新的首选。
属性名占位符的用法类似,比如条件是#st = :st,然后在expressionAttributeNames里声明#st -> status。一个细节是占位符名字不能重复定义,同一个表达式里#st只能映射到一个真实属性名,否则SDK会在本地校验阶段就报错。
ConditionalCheckFailedException的处理与常见误区
条件不满足时,DynamoDB不会静默失败,而是抛出ConditionalCheckFailedException。很多使用者看到异常就以为是程序bug,其实这是条件表达式正常工作的信号,正确的处理方式是捕获它并执行业务逻辑,比如提示重试、重新读取数据做合并等:
try {
dynamoDbClient.putItem(request);
} catch (ConditionalCheckFailedException e) {
// 条件不满足,说明记录已存在或版本冲突
// 这里做重试或返回业务错误码,而不是把异常当成系统故障
log.warn("版本冲突,当前写入被拒绝");
throw new OptimisticLockException("数据已被其他请求修改,请刷新后重试");
}
第一个常见误区是以为条件表达式可以用在GetItem或Query上。实际上条件表达式只对写操作生效,读操作没有条件参数。如果需要在读取时过滤,应该用FilterExpression,但要注意FilterExpression是在读取之后应用,会消耗相同的读容量,性能上和条件表达式完全不是一回事。
第二个误区是在事务里的行为差异。TransactWriteItems支持条件表达式,且只要其中任何一个item的条件失败,整个事务都会回滚;但BatchWriteItem根本不支持条件表达式,如果需要批量写入且带条件校验,只能循环调用单个写接口或改用事务。这一点在做迁移脚本或批量导入时经常踩坑,有人写完BatchWriteItem才发现条件根本没生效,数据已经重复插入了。
第三个误区是复合条件的优先级。attribute_not_exists(id) OR #st = :done这种写法里,OR的短路逻辑和直觉一致,但如果和AND混用,必须用括号显式分组,例如(attribute_not_exists(id) OR #st = :done) AND #type = :t。DynamoDB不会帮你猜测优先级,漏写括号往往导致语义完全偏离预期。
典型应用场景与替代方案
条件表达式的三大典型场景可以总结为:防重复插入、乐观锁控制、状态机流转。防重复插入用attribute_not_exists(pk);乐观锁用version = :v配合ADD version :one;状态机流转则类似#st = :pending,只有当前状态是pending时才允许改成processing,避免并发下状态被跳过或回退。
Node.js环境下用DocumentClient写起来更直观一些,值不需要手动包装类型:
const params = {
TableName: "Orders",
Key: { orderId: "o-2024-001" },
UpdateExpression: "SET #st = :newSt",
ConditionExpression: "#st = :expectSt",
ExpressionAttributeNames: { "#st": "status" },
ExpressionAttributeValues: { ":expectSt": "PAID", ":newSt": "SHIPPED" }
};
try {
await docClient.update(params).promise();
console.log("状态流转成功");
} catch (e) {
if (e.code === "ConditionalCheckFailedException") {
console.log("订单状态已被其他流程修改,本次流转取消");
} else {
throw e;
}
}
如果条件逻辑复杂到需要跨多条item判断,比如“不存在同用户的未完成订单”,条件表达式就无能为力了,因为它只能看当前这一条item。这时候的替代方案有三种:一是把判断条件物化到这条item本身(例如用一个组合主键让“未完成订单”天然唯一);二是先Query再决定是否写入,但要接受窗口期内并发的风险;三是使用TransactWriteItems配合条件表达式在事务层面保证一致性。多数场景下,通过合理设计主键让数据库层面的唯一性约束替你兜底,比应用层判断更可靠。
最后提醒一点:条件失败会消耗一次写容量单位(WCU),因为DynamoDB确实执行了校验。在高并发冲突频繁的表上,重试风暴会带来额外的容量开销,建议给重试加上指数退避,并在监控里单独跟踪ConditionalCheckFailedException的频率——它突然升高通常意味着业务上出现了预期外的并发冲突,值得及时排查。
DynamoDBcondition expression条件表达式修改时间:2026-09-04 17:24:44