了解什么是 Amazon DynamoDB、其 NoSQL 模型如何工作,以及用于构建可扩展、低延迟系统与微服务的实用设计模式。

Amazon DynamoDB 是 AWS 提供的完全托管 NoSQL 数据库服务,面向需要在几乎任意规模下保持一致低延迟读写的应用。“完全托管”意味着 AWS 负责基础设施工作——硬件配置、复制、打补丁和许多运维任务——因此团队可以把精力放在交付功能而不是运行数据库服务器上。
DynamoDB 的核心是将数据以item(行)存储在table 中,但每个 item 可以有灵活的属性。其数据模型可被理解为键值与文档模型的混合:
当团队希望获得可预测的性能并简化运维,且工作负载不适合关系型连接(joins)时,通常会选择 DynamoDB。常见用途包括微服务(各服务拥有自己的数据)、流量突发的无服务器应用,以及对数据变化做出反应的事件驱动系统。
本文将介绍构建块(表、键、索引)、如何围绕访问模式建模(包括单表设计)、扩展与容量模式如何工作,以及把变更流式化进事件驱动架构的实践模式。
DynamoDB 围绕一些简单的构建块组织,但这些细节决定了你如何建模数据以及请求的速度(和成本)。
表(table) 是顶层容器。表中的每条记录是一个 item(类似一行),每个 item 是若干 attributes(类似列)。
与关系型数据库不同,同一表内的 items 不需要共享相同的属性。一个 item 可能有 {status, total, customerId},另一个可能包含 {status, shipmentTracking}——DynamoDB 不要求固定模式。
每个 item 都由一个 主键 唯一标识,DynamoDB 支持两种类型:
在实践中,复合键支持按组访问的模式,比如“某个客户的所有订单,按时间新到旧”。
Query 按 主键(或索引键)读取 item。它针对特定分区键,并能按排序键范围过滤——这是高效且首选的路径。
Scan 则会遍历整个表(或索引),然后再过滤。它容易上手,但在规模化时通常更慢且更昂贵。
一些早期会遇到的约束:
这些基础会影响后面的访问模式、索引选择以及性能特性。
DynamoDB 常被描述为既是键值存储又是文档数据库。这是准确的,但理解各自含义对日常设计很有帮助。
核心是通过键检索数据。给出主键值,DynamoDB 返回单个 item。这种键控查找为许多工作负载带来可预测的低延迟存储。
同时,item 可以包含嵌套属性(maps 和 lists),使其具有文档数据库的感觉:你可以存储结构化负载,而无需提前定义严格模式。
Items 自然对应类 JSON 的数据:
profile.name, profile.address)。当一个实体通常作为整体被读取时(如用户资料、购物车或配置包),这种方式非常合适。
DynamoDB 不支持服务端连接(joins)。如果应用需要在一个读取路径中获取“订单及其行项和运输状态”,通常会选择去规范化:把某些属性复制到多个 item,或将小的子结构直接嵌入到 item 中。
去规范化会增加写入复杂性,可能导致更新扇出(update fan-out)。其回报是减少往返请求和加速读取——这在可扩展系统中往往是关键路径。
最快的 DynamoDB 查询是你能表达为“给我这个分区”(可选地“在该分区内给我这个范围”)。因此,键的选择更多是关于如何读取数据,而不仅是如何存储它。
分区键 决定了 item 存放在哪个物理分区。DynamoDB 对该值做哈希以分散数据与流量。如果大量请求集中在少数分区键值上,会产生“热”分区并触及吞吐限制,即便表大部分时间空闲。
良好的分区键应:
"GLOBAL")有了 排序键,共享同一分区键的 items 会被存放在一起并按排序键排序。这使得以下操作高效:
BETWEEN、begins_with)常见做法是组合排序键,例如 TYPE#id 或 TS#2025-12-22T10:00:00Z,以支持多种查询形态而不需额外表。
PK = USER#<id>(简单的 GetItem)PK = USER#<id>,SK begins_with ORDER#(或 SK = CREATED_AT#...)PK = DEVICE#<id>,SK = TS#<timestamp>,用 BETWEEN 查询时间窗口如果你的分区键与最高流量的查询对齐并且能均匀分布,你将获得稳定的低延迟读写。否则,你将通过扫描、过滤或额外索引来补偿——这些都会增加成本并提高出现“热键”的风险。
二级索引为 DynamoDB 提供除了表主键之外的备用查询路径。当新的访问模式出现而不适用于原始键设计时,可以添加索引来为相同 items 重新键值以支持不同查询。
全局二级索引(GSI) 有自己的分区键(可选排序键),可以与主表完全不同。它是“全局的”,跨表的所有分区,且可以随时添加或删除。需要新的访问路径(例如按 customerId 查询订单,而表以 orderId 为键)时使用 GSI。
本地二级索引(LSI) 与基表共享相同的分区键但使用不同的排序键。LSI 必须在建表时定义。它们适用于在同一实体组内需要多种排序顺序的情形(比如按 createdAt 与按 status 两种顺序获取客户订单)。
投影决定 DynamoDB 在索引中存储哪些属性:
对基表的每次写入可能触发写入到一个或多个索引。更多的 GSI 和更宽的投影会增加写成本和容量消耗。围绕稳定的访问模式规划索引,并尽可能把投影属性保持最小。
DynamoDB 的扩展从一个选择开始:按需(On-Demand) 或 预配置(Provisioned)。两者都能达到很高吞吐量,但在变化流量下表现不同。
按需 最简单:按请求付费,DynamoDB 会自动适应可变负载。适合流量不可预测、处于早期或有突发负载的场景,不想管理容量目标的团队。
预配置 则是容量规划:你指定(或自动扩缩)读写吞吐量,在稳定使用下通常能获得更可预测的成本。适用于能预测需求的工作负载。
预配置吞吐以:
实际成本受 item 大小、一致性要求和是否扫描等访问模式影响:较大的 item、强一致与扫描会快速消耗容量。
自动伸缩会基于利用率目标调整预配置的 RCU/WCU。它有助于逐步增长和周期性负载,但并非瞬时。突然的峰值仍可能被限制,如果容量来不及扩展也无法解决热点分区问题。
DynamoDB Accelerator(DAX) 是一个内存缓存,可以降低读取延迟并减轻重复读取的负载(例如热门商品页面、会话查找、排行榜)。它在大量客户端重复请求同一 items 时最有用;对于写密集型模式帮助有限,也不能替代精心的键设计。
DynamoDB 允许在读取保证与延迟/成本之间做权衡,因此针对每个操作明确“正确性”含义很重要。
默认 GetItem 和 Query 使用 最终一致 读取:写入后短时间内可能读到旧值。对于资讯流、商品目录等读取多、容忍短暂陈旧的视图通常没问题。
强一致 读取(单区基表可用)保证你能看到最新确认的写入。强一致会消耗更多读取容量并可能增加尾延迟,请仅在真正关键的读取场景使用。
强一致适用于决策会触发不可逆动作的读取:
对于计数器,最安全的做法通常不是“强读然后写”,而是使用原子更新(例如 UpdateItem 的 ADD),以免丢失增量。
DynamoDB 事务(TransactWriteItems、TransactGetItems)提供跨最多 25 个 item 的 ACID 语义。适用于必须一起更新多项(如同时创建订单与保留库存)或需要不容许中间态的不变式场景。
分布式系统中重试是正常的。使写入具备幂等性,避免重试导致副作用重复:
ConditionExpression 强制唯一性(例如 attribute_not_exists)在 DynamoDB 中,正确性主要关乎选择合适的一致性级别并设计可安全重试的操作。
DynamoDB 将表数据分布在多个物理分区中。每个分区对读写吞吐和存储容量都有上限。你的分区键决定了 item 的存放位置;如果太多请求指向同一个分区键值(或一小撮值),该分区就会成为瓶颈。
热分区通常由集中流量的键设计引起:诸如 USER#1、TENANT#default 或 STATUS#OPEN 之类的“全局”分区键,或时间序列中所有写入都指向“现在”这一键。
通常你会看到:
ProvisionedThroughputExceededException)先为分布设计,然后顾及查询便利性:
TENANT#<id> 而不是共享常量)。ORDER#<id>#<shard> 以分散写入到 N 个 shard,再在需要时跨分片查询。METRIC#2025-12-22T10)以避免所有写入落到最新一项。对于不可预测的突发,按需 容量可以在服务限额范围内吸收突发流量。使用 预配置 时,结合自动伸缩并在客户端实现带抖动的指数退避(exponential backoff with jitter)以避免同步重试放大峰值。
DynamoDB 的数据建模从 访问模式 出发,而不是 ER 图。你要根据需要快速的查询来设计键,使常见查询成为快速的 Query 操作,其他不常用的查询要么避免,要么以异步方式处理。
“单表设计”指在一张表中存放多种实体类型(用户、订单、消息),并使用一致的键约定来在一个 Query 中获取相关数据。这样可以减少跨实体往返并保持延迟可预测。
常见做法是使用复合键:
PK 分组逻辑分区(例如 USER#123)SK 在该组内排序(例如 PROFILE、ORDER#2025-12-01、MSG#000123)这样你可以通过选择排序键前缀来获取“某用户的所有内容”或“仅用户的订单”。
对于图结构关系,邻接表(adjacency list) 很适合:把边作为 item 存储。
PK = USER#123, SK = FOLLOWS#USER#456若需反向查找或真正的多对多,可以添加反向边 item 或将其投影到 GSI,视读取路径而定。
对于事件和指标,避免无界分区可以通过分桶实现:
PK = DEVICE#9#2025-12-22(设备 + 日期)SK = TS#1734825600(时间戳)使用 TTL 自动删除过期点,并把聚合(小时/天汇总)作为单独 item 保存以便快速仪表盘查询。
如果想更深入复习键约定,请参阅 /blog/partition-key-and-sort-key-design。
DynamoDB Streams 是内置的变更数据捕获(CDC)机制。启用表的 Streams 后,每次插入、更新或删除都会产生一个流记录,供下游消费者响应——无需轮询表。
流记录包含键以及(可选)item 的旧像与新像,取决于你选择的 stream view type(仅键、新镜像、旧镜像或两者)。记录被分组为 shard,需按顺序读取。
常见的架构是 DynamoDB Streams → AWS Lambda,每批记录触发一个函数。也可以使用其他消费者(自定义消费者,或将记录推入分析/日志系统)。
典型工作包括:
这将主表保持为低延迟读写的优化目标,并将派生工作推给异步消费者。
Streams 在每个 shard 内提供有序处理(通常与分区键相关),但全局没有顺序。投递是至少一次,因此可能产生重复。
为安全处理:
如果按这些保证设计,Streams 能把 DynamoDB 打造为事件驱动系统的可靠骨干。
DynamoDB 通过在一个区域的多个可用区间分布数据来实现高可用性。对大多数团队来说,实用的可靠性收益来自清晰的备份策略、理解复制选项以及监控适当的指标。
按需备份 是你在想要已知恢复点时手动(或自动)创建的快照——如在迁移前、发布后或大规模回填前。适合作为“书签”点。
时间点恢复(PITR) 持续捕获更改,以便你能将表恢复到保留窗口内的任意一秒。PITR 是应对误删、错误部署或未被校验的写入的安全网。
若需多区域弹性或接近用户的低延迟读取,Global Tables 可在所选区域之间复制数据。它们简化故障切换,但引入跨区复制延迟与冲突解决的考虑——因此要明确写入模式与 item 的所有权。
至少应监控并告警:
这些信号通常能揭示热分区问题、容量不足或意外的访问模式。
遇到节流,先定位导致节流的访问模式,然后通过临时切换到按需或增加预配置容量来缓解,并考虑对热点键做分片。
对于部分故障或错误升高,缩小影响范围:禁用非关键流量、使用带抖动的重试并优雅降级(例如提供缓存的读取)直到表恢复稳定。
DynamoDB 的安全主要关乎收紧“谁”能调用哪些 API、从“哪里”调用以及对哪些键有权限。因为表可能包含多种实体(有时是多租户),访问控制应与数据模型一并设计。
从基于身份的 IAM 策略开始,限制动作(例如 dynamodb:GetItem、Query、PutItem)到最小集合,并将作用域限定到特定表 ARN。
对于更细粒度的控制,使用 dynamodb:LeadingKeys 按分区键值限制访问——当某个服务或租户只能访问其自己的键空间时这非常有用。
DynamoDB 默认对静态数据进行加密,使用 AWS 管理密钥或客户管理的 KMS 密钥。如果有合规要求,请核实:
对于传输中加密,确保客户端使用 HTTPS(AWS SDK 默认如此)。如果在代理处终止 TLS,请确认代理与 DynamoDB 之间的跳转仍然是加密的。
使用 VPC Gateway Endpoint 访问 DynamoDB,使流量停留在 AWS 网络内,并能通过端点策略约束访问。配合出站控制(NACL、安全组和路由)以避免“任何流量都能到公网”的路径。
对于共享表,在分区键中包含租户标识(例如 TENANT#<id>),然后用 IAM 条件在 dynamodb:LeadingKeys 上强制租户隔离。
如果需要更强的隔离,请考虑为每个租户或每个环境使用独立表;当运维简便性与成本效率优先于更严格的影响半径时,才采用共享表设计。
DynamoDB 常常表现为“当你精确时便宜,当你模糊时昂贵”。账单通常跟随访问模式,因此最好的优化从明确这些模式开始。
账单主要由:
一个常见的惊讶是:对基表的每次写入也会写入到每个受影响的 GSI,因此“再加一个索引”可能会成倍增加写入成本。
良好的键设计能减少对昂贵操作的需求。如果你频繁使用 Scan,就是在为扔掉的数据付费。
优先:
Query(可选排序键条件)如果某个访问模式很少见,考虑用单独表、ETL 作业或去规范化的读模型来服务,而不是永远保留一个 GSI。
使用 TTL 自动删除短寿命项(会话、临时令牌、中间工作流状态)。这能削减存储并长期保持索引较小。
对于追加型数据(事件、日志),将 TTL 与能按“仅最近”查询的排序键设计结合,避免经常触及冷历史数据。
在预配置模式下,设置保守基线并基于真实指标进行自动伸缩。在按需模式下,关注低效模式(大 item、频繁调用)驱动的请求量。
把 Scan 当作最后手段——当确实需要全表处理时,应在非高峰时段以分页与退避的受控批处理方式执行。
当你的应用能被表达为一组明确的访问模式,并且需要在高负载下保持一致的低延迟时,DynamoDB 非常出色。如果你能事先描述好读取与写入(按分区键、排序键以及少量索引),它通常是最易于运维的高可用数据存储之一。
DynamoDB 适合:
若核心需求包括:
则应考虑其他方案。
很多团队把 DynamoDB 用于“热”操作读写,然后补充:
当你在验证访问模式与单表约定时,速度很重要。团队有时会在 Koder.ai 上快速原型前端与服务,然后随着真实查询出现迭代 DynamoDB 的键设计。即便生产后端不同,快节奏的端到端原型能帮助揭示哪些查询应为 Query,哪些会不小心变成昂贵的 Scan。
验证: (1) 你的核心查询是已知且基于键的,(2) 正确性需求与一致性模型匹配,(3) 预期的 item 大小与增长已被理解,(4) 成本模型(按需 vs 预配置 + 自动伸缩)符合预算。
DynamoDB 是 AWS 提供的完全托管的 NoSQL 数据库,擅长在非常大规模下提供一致的低延迟读写。团队在能以键为中心描述访问模式(按 ID 获取、按拥有者列出、时间范围查询等)并希望避免管理数据库基础设施时,会选择它。
它在微服务(每个服务拥有自己的数据)、无服务器应用以及基于事件的系统中尤为常见。
表(table)包含 items(类似行)。每个 item 是一组灵活的 attributes(类似列),可以包含嵌套的数据。
当一次请求通常需要“整个实体”时,DynamoDB 非常适合,因为 item 可以包含 maps 和 lists(类 JSON 结构)。
仅有 分区键(partition key) 的主键可以唯一标识一个 item(简单主键)。
分区键 + 排序键(sort key) 的组合(复合主键)允许多个 item 共享相同的分区键,同时通过排序键保持唯一并定义顺序。
复合键支持的常见模式包括:
Query 在你能指定分区键(可选地加上排序键条件)时使用——这是高效且可扩展的路径。
Scan 则会遍历整个表或索引然后再过滤,通常更慢且更昂贵。频繁扫描通常意味着你的键或索引设计需要调整。
二级索引提供“替代的查询路径”。
索引会增加写入成本,因为写入会被复制到索引中。
如果流量不可预测、突发或你不想管理容量,请选 按需(On-Demand)。按照请求付费。
如果流量稳定且可预测,想要更可控的成本,则选 预配置(Provisioned),并结合自动伸缩。不过它对突发流量的反应不是瞬时的。
默认情况下,读取是 最终一致(eventually consistent),写入之后短时间内可能会读到旧值。
当需要“必须是最新”的检查(例如权限判断、工作流状态)时,使用 强一致(strongly consistent) 读取。对并发正确性,通常优先使用 原子更新(例如 UpdateItem 的 ADD 或条件写)而不是先读后写的循环。
事务(TransactWriteItems、TransactGetItems)在最多 25 个项上提供 ACID 语义。
当必须同时更新多项(例如创建订单并预留库存)或需要不可容忍中间状态的不变式时使用。事务会增加成本并带来额外延迟,因此仅在必要的流程中使用。
热键/热分区发生在过多请求集中到同一分区键值(或一小撮值)时,即使表整体负载不高,该分区也会被瓶颈化,导致节流。
常见缓解措施包括:
启用 DynamoDB Streams 可以得到写入/更新/删除的变更流。常见模式是 Streams → Lambda 来触发下游工作。
需要注意的保证:
因此消费者应保证 幂等(按主键 upsert、用条件写或记录已处理的事件 ID)。