DynamoDB 存储内部原理如何工作
DynamoDB 是一张哈希表,它的值是被排序的树,散布在三个可用区的机器上。两个数据结构干了 几乎所有的活儿:partition key 上的一个哈希挑机器,sort key 上的一棵 B 树给里面的 item 排序。
DynamoDB 如何存储数据?
DynamoDB 将你的数据存储为一张巨大的分布式哈希表,其值是有序的 B 树。partition key 上的哈希选定一个存储节点;B 树则按 sort key 在节点内部给 item 排序。每次写入在确认前都会同步复制到跨三个可用区的三个节点。
- partition key 是被哈希的,不是被搜索的。 DynamoDB 在你的
PK上跑一个哈希函数,找到 持有那个分区的唯一存储节点——一次 O(1) 跳转,与表大小无关。 - sort key 住在一棵 B 树里。 在一个分区内部,item 存在一棵按
SK字典序排序的 B 树里, 这就是为什么范围读取(begins_with、between)便宜而Scan不便宜。 - 每次写入在确认前命中三个节点。 一次写入去一个 leader,它同步复制到另外两个可用区的
peer——在你的
PutItem返回 之前 就买下了持久性。 - 这就是访问模式规则存在的原因。 先哈希再走树只在你按 key 读时才快。没有 key,就没有 快路径——你退回到扫描那棵树。
从数据结构开始,不是从 API
从 SQL 过来,你把表想象成磁盘上的行加一个挑索引的查询规划器。DynamoDB 没有规划器。存储布局 就是 那个合约——什么快、什么是自坑,两者都直接从两个结构里掉出来。
想象一张巨大的分布式 map。key 是你 partition key 的哈希。值不是单个 item——它是一整棵 共享那个 partition key 的 item 的 B 树,按 sort key 排序。
别的一切——query 语义、那些 10 GB 警告、为什么一个缺失的 key 逼出一个 Scan——都是那一句话
的后果。
哈希 partition key 来找节点
当一个请求到来,DynamoDB 对 partition key 值施加一个内部哈希函数。哈希确定性地映射到一个 存储节点——拥有那些 item 的物理分区。AWS 把这记录为常数时间 key 查找背后的机制,与表 大小无关。
那是那个 O(1) 步骤。一张 10 TB 的表和一张 10 KB 的表 定位 起来花同样的代价:哈希、跳转、 完成。没有索引扫描去找节点,没有统计信息,没有计划。
要害是反面。如果你不提供 partition key,DynamoDB 就没有节点可跳——它得走每一个分区。那是一个
Scan,是 O(1) 和读取整张表之间的差别。
请求哈希到恰好一个分区,然后下降那个分区的 sort key B 树到 item——是两个便宜的步骤,而不是 一次表遍历。
在每分区 B 树里给 item 排序
在单个分区内部,item 不是一个堆。它们被持在一棵 以 sort key 为 key 的 B 树 里,按字典序
排序。一次 B 树搜索是 O(log n),而且关键是那个 n 是 一个 分区里的 item,不是整张表。
这就是 sort key 范围读取便宜的全部原因。拿一张车队遥测表,每台设备的读数住在一个 partition key 下:
| PK | SK |
|---|---|
| PK = DEVICE#a91 | SK = READING#2026-06-23T08:00Z |
| PK = DEVICE#a91 | SK = READING#2026-06-23T08:05Z |
| PK = DEVICE#a91 | SK = READING#2026-06-23T08:10Z |
因为 B 树是排序的,"08:00 到 09:00 之间的所有读数"是一次到起始值的树下降加一次顺序行走—— 不是一次对那台设备曾发过的每条读数的过滤。你只读匹配的那个范围。
那个排序也是为什么一个 begins_with(SK, "READING#2026-06-23") query 快而对一个非 key 属性
过滤不快。树能按 SK 寻位;它不能按别的任何东西寻位。要安全地组合那些 key 条件,在
DynamoDB 表达式构建器里构建它们,而不是手工拼接字符串:
KeyConditionExpression PK = :pk AND begins_with(SK, :day)
把每次写入复制到三个可用区
一个分区不是一台机器。每一个都跨 三个可用区的三个节点 复制——一个可以追溯到 2007 年 Amazon Dynamo 论文的复制模型的设计血脉。
一个节点是这个分区的 leader。一次写入去 leader,它在本地写并复制到它的两个 peer。一旦
一个持久的法定多数节点拥有它,leader 就确认这次写入——所以跨可用区的持久性是在你的 PutItem
返回 之前 付清的。
读取有得选。一次 强一致 读取去 leader,看到最新已提交的写入。一次 最终一致 读取可以 由三个节点中的任一个服务,其中一个可能落后几毫秒——那就是你为更便宜、更可用的读取换来的滞后。
| 最终一致 | 强一致 | |
|---|---|---|
| 由谁服务 | 三个节点中的任一个 | 仅 leader 节点 |
| 看到最新写入 | 也许(小滞后) | 总是 |
| RCU 成本 | 一半 | 全额 |
| 可用性 | 更高 | 更低(单节点) |
同样的异步传播思想正是为什么一个 GSI 读取可能陈旧——见 GSI 是最终一致的。
从结构里读出规则
几乎每一条 DynamoDB"规则"都只是存储物理。
- 总是提供 partition key。 没有 key,没有哈希目标——你在扫描整张 map。这是 Query vs Scan的核心。
- 把你一起读的东西放在一处,放在一个 partition key 下,这样一次哈希加一次树行走就返回 整个 item 集合。那是 单表设计的根基。
- 让分区保持有界。 一个分区是有限节点集上的一棵 B 树;一个失控的热 key 或一个 LSI 的 10 GB 上限都是那个物理分区的限制。
一旦你看清这个先哈希再 B 树的形态,访问模式纪律就不再感觉随意了——你只是在让每一次读取都待 在快路径上。
下一步
用 sort key 策略和 单表设计把你的 key 建模得匹配这个结构,然后在 DynamoDB 表达式构建器里组装实际的表达式。 试用 DynoTable,看着这些读取对你自己的表运行,并精确看到一个 key 条件拉回哪些 item。