DynamoDB 条件表达式
条件表达式是 DynamoDB 在提交你的写入之前对现有项求值的一个谓词。如果谓词为假,写入被拒绝,
什么都不改变。它是 DynamoDB 对写入最接近 WHERE 子句的东西——也是强制不变量的唯一安全方式。
DynamoDB 条件表达式如何工作?
条件表达式是 DynamoDB 在提交一次写入之前、在服务端针对当前项求值的一个谓词。如果它为真,写入继续进行;如果为假,写入以 ConditionalCheckFailedException 被拒绝,什么都不改变。它把检查与变更折进同一个原子操作,因此并发的调用者无法竞争一次陈旧读取。
- 它是守卫,不是过滤。
ConditionExpression在服务端针对当前项运行;一个为假的结果以ConditionalCheckFailedException让写入失败。 - 它取代读后写。 没有
SELECT再UPDATE的往返——检查和变更是一个原子操作,所以两个调用者 无法竞争。 - 拒绝免费,运行不免费。 一次失败的条件写入仍然消耗写入容量。这个保证花费的成本与它阻止的那次 写入相同。
从 SQL 过来,你会读那一行,在应用代码里检查它,然后更新。在 DynamoDB 中,读与写之间那道缝隙是一个 等待并发调用者的数据损坏 bug。条件表达式合上了这道缝隙。
它们适用于哪里
你把一个 ConditionExpression 附加到 PutItem、UpdateItem、DeleteItem,以及
TransactWriteItems 内部的每个动作。它不是 Query 或 Scan 的一部分——那些用
FilterExpression,那是读取路径上一个不同的东西。
那个区分会绊倒人,所以要精确:
ConditionExpression | FilterExpression | |
|---|---|---|
| 路径 | 写入(Put/Update/Delete) | 读取(Query/Scan) |
| 失败时的效果 | 拒绝整个写入 | 把该项从结果中丢弃 |
| 看到的 | 当前项,写入前 | 每个候选项,读取后 |
| 成本 | 失败的写入仍然计费 | 被过滤的项仍然为该读取计费 |
两者都在服务端运行。区别在于"假"做什么:一个条件中止一次变更;一个过滤只是隐藏一行你已经付费读取的。 (AWS: 条件表达式)
你真正会用到的函数
条件语言很小。主力是:
attribute_exists(path)/attribute_not_exists(path)—— 这个属性在项上存在吗?经典的"仅当不存在 时创建" / "仅当存在时更新"惯用法。- 比较符 ——
=、<>、<、<=、>、>=—— 对一个值或另一个属性。 attribute_type、begins_with、contains、size—— 类型与字符串/集合检查。BETWEEN … AND …、IN (…)—— 范围与成员资格。AND、OR、NOT、括号 —— 组合上面这些。
分区键上的 attribute_not_exists 是让 PutItem 表现得像一个不会覆盖现有项的插入的标准做法
——DynamoDB 没有单独的"插入"操作,所以条件就是插入语义。
(AWS: 比较运算符与函数参考)
演练实例:守卫一个账本不被透支
拿一个银行账本。每个账户是一个项:
PK = "ACCT#a7f3"
SK = "BALANCE"
clearedCents = 50000
holdCents = 0不变量:一笔扣款绝不能把可用余额压到零以下,而且你绝不能扣一个不存在的账户。两条规则,都能在写入 本身里强制。
错误做法(那个坑)
GetItem ACCT#a7f3 / BALANCE → clearedCents = 50000
if (50000 >= 30000) ... ← app-side check
UpdateItem SET clearedCents = 20000
在 GetItem 和 UpdateItem 之间,第二笔扣款能读到同样的 50000、通过它自己的检查、然后也写入。
两者都成功;账户变成负数。这是一场读-改-写竞争,再多的应用侧校验也修不了它——检查和写入是分离的操作。
正确做法
把检查折进写入。扣 30000 分,条件是账户存在并且持有足够:
UpdateItem ACCT#a7f3 / BALANCE
SET clearedCents = clearedCents - :amt
ConditionExpression:
attribute_exists(PK) AND clearedCents >= :amt配 :amt = 30000。如果余额太低,或者该项从未被创建,DynamoDB 以 ConditionalCheckFailedException
拒绝写入,余额不受触动。并发的那笔扣款要么看到原始余额并被对照它检查,要么看到更新后的——绝不会
是它据以行动的一个陈旧读取。
你可以用 DynamoDB 表达式构建器 构建并复制确切的表达式
——名称、值,全都有——而不必手工拼装 ExpressionAttributeValues 映射。
在 DynoTable 中查看守卫
当一次条件写入失败时,你想看到项的真实状态,而不是去猜它。把账户项拉出来,直接读 clearedCents。

读懂拒绝,别盲目重试
ConditionalCheckFailedException 不是一个瞬时错误——重试同一次写入什么也不改变。它意味着一条业务
规则触发了:资金不足、重复创建、版本陈旧。把它当作一个领域结果来呈现,而不是一次基础设施抖动。
有两样东西让失败可调试:
ReturnValuesOnConditionCheckFailure: ALL_OLD—— DynamoDB 在失败时连同返回当前项,于是你可以 显示"余额是 20000,你请求了 30000"而无需第二次读取。 (AWS: 处理项)- 区分两种失败原因。
attribute_exists(PK) AND clearedCents >= :amt把"没有账户"和"没有资金" 坍缩成一个异常。如果调用者需要把它们分开,就拆成两次写入,或检查返回的项。
乐观锁是同一个把戏
版本号模式不过是戴了另一顶帽子的条件表达式。存一个 version 属性;每次写入都断言你读到的版本并把它
加一:
UpdateItem ACCT#a7f3 / BALANCE
SET clearedCents = :new, version = :next
ConditionExpression: version = :seen如果另一个写入者先动了,version = :seen 为假,写入被拒绝,你重新读取并重试。这就是 DynamoDB 在没有
锁的情况下做并发控制的方式——断言你看到的,若它动了就失败。(AWS: 用版本号做乐观锁)
陷阱与下一步
- 与保留字冲突的名称。
status、size、name以及约 570 个其他词是保留的。用ExpressionAttributeNames(#s = status)给它们起别名,否则你的表达式会悄悄解析失败。 - 一个条件无法引用另一个项。 它只看到正在被写入的那个项。跨项不变量需要
TransactWriteItems配每个动作的ConditionExpression,或对一个哨兵项做一次ConditionCheck。 - 失败的写入仍然花 WCU。 一个 90% 的时候都拒绝的守卫,仍然为那些拒绝计费。便宜的保险,但不免费。
要建模这些守卫所针对的键,参见 单表设计和 Query 与 Scan。当你准备好对真实数据发起条件写入时,下载 DynoTable 并对你自己的表运行它们。


