进阶阅读约 3 分钟

DynamoDB 分区键如何工作

你的不是一列——它是一个地址。DynamoDB 对这个键做哈希,而哈希决定了哪台物理机器存放这个项目。键选得好,负载就摊开;选得差,一台服务器就得独自扛下压力。

DynamoDB 分区键如何工作?

DynamoDB 把你的送进一个内部哈希函数,而那个哈希决定了哪个物理分区存放这个项目。这个键不像 SQL 列那样被排序或索引——它是一个地址。选一个高基数的键,负载就摊开到许多分区;选一个低基数的键,单个分区就得扛下全部压力。

  • 键是被哈希的,不是被排序的。 DynamoDB 把你的分区键送进一个内部哈希来挑选分区。两个相邻的值在磁盘上落得八竿子打不着。
  • 一个分区是一个真实的存储单元。 每个大约上限为 10 GB、每秒 3,000 读取单元、每秒 1,000 写入单元。你的流量要被你的键摊到多少个分区上一除。
  • 热键是那个坑。 把大多数请求都灌向一个分区键值,你就会在那个分区上遭遇限流,而表的其余部分闲坐着。
  • 高基数的键才赢。 你拥有的、被均匀命中的不同键值越多,能吸收负载的分区就越多。

先弄清这个键究竟在做什么

从 SQL 过来,主键是一个被排序、被索引的列,你在它上面做 JOINORDER BY。在 DynamoDB 里,分区键(有时叫哈希键)做的是另一回事:它决定_放置位置_。

DynamoDB 把分区键喂进一个内部哈希函数。输出映射到一个键空间,而键空间被切成一段段区间——每段区间由一个物理分区拥有。那个分区是一个真实节点上的真实存储。

所以分区键回答一个问题:哪台机器持有这个项目? (如果你有的话)只在那台机器_内部_给项目排序。它在放置位置上不起任何作用。

跟着一次写入穿过哈希

假设你运营一个采集设备读数的 SaaS。你的表 SensorReadings 用一个分区键 deviceId 和一个排序键 readingTs。你为 deviceId = "vac-7741" 写入一条读数。

这就是那次写入所走的路径——从你的键,到它落上的磁盘:

键空间区间PutItemdeviceId = 'vac-7741'对分区键做哈希哈希映射到键空间中的一点哪段区间拥有它?分区 P2项已存储, readingTs 排序

vac-7741 的写入被哈希到键空间中的一个点,那个点落在 P2 的区间里,于是项目落上 P2——在那里按 readingTs 排序。

要内化的一点是:"vac-7741""vac-7742" 只差一个字符,但它们的哈希毫不相关。它们几乎必然住在不同的分区上。分区键空间里没有「相邻」这回事。

这是 DynamoDB 从最初的设计继承来的一致性哈希思想——2007 年的 Amazon Dynamo 论文(「Dynamo: Amazon's Highly Available Key-value Store」)正是通过哈希把键铺散到各节点,好让没有单个节点成为瓶颈。

在下面粘贴一串分区键值,看看一个哈希如何把它们撒进各个桶。一个高基数的集合会均匀摊开;重复用同一个值,它就会全都堆进单个桶——正是下一节要讲的

分区键分布

每行一个分区键值。重复某个值即可模拟热点键。

8 个桶8 个键
  • #0
    0
  • #1
    1
  • #2
    1
  • #3
    0
  • #4
    2
  • #5
    2
  • #6
    0
  • #7
    2

这是一个简化的教学用哈希,并非 DynamoDB 真实的内部哈希。DynamoDB 使用一个未公开的内部函数,且分区数会随表的增长而增加 —— 请仅将其用于建立直觉,理解不同的键如何分散、单个热点键又如何堆积。

这是一个用于建立直觉的教学哈希,不是 DynamoDB 真正的内部哈希——真实的函数、键空间和分区边界都是 AWS 内部实现。用它来培养对「摊开 vs 倾斜」的感觉,而不是用来预测某个键会落到哪个物理分区。

尊重分区的硬性上限

一个物理分区是有限的。按照 AWS DynamoDB 开发者指南,每个大约至多容纳:

上限每个分区
存储~10 GB
读取吞吐每秒 3,000 读取单元
写入吞吐每秒 1,000 写入单元

当一个分区被填过 10 GB,或你的预置吞吐需要更多空间时,DynamoDB 会拆分它——键空间区间被切开,项目重新分布到更多分区上。这是自动的;你不会去触发它。

坑在于:一次拆分可以在排序键边界处把某个分区键的项目集合切开,于是一个繁忙键的负载能摊到更多分区上。拆分救不了的是单个热_项目_、一个不断增大的排序键,或一张带 LSI 的表——这些都会把集合钉死在一个分区上。

给这个陷阱起个名字:热分区

热分区是那个经典的坑。它发生在一个分区键值(或极少数几个)吸收了不成比例的流量份额时。

具体的翻车场景:你把 SensorReadings 换成一个分区键 region,取值像 "us-east""eu-west"。三个地区意味着三个键值,意味着——至多——三个分区在干真活。用读取猛砸 "us-east",它就会在 3,000 RCU 处限流,而表的总预置容量却闲置着。

DynamoDB 的自适应容量能缓和这一点——它可以把闲置的吞吐挪向一个繁忙分区,并把单个极热的键隔离到它自己的分区上。AWS 在 re:Invent 的「Advanced Design Patterns for DynamoDB」深入讲座里详述过这一点。但自适应容量买来的是时间,不是免疫:单个热_项目_、一个不断增大的排序键,或一个 LSI,仍然会把一个键的上限压在单个分区上。要为摊开而设计;别指望这张安全网。

选一个高基数的键

修复之道是基数——不同键值的数量,以及流量命中它们有多均匀。

  • 低基数regionstatustrue/false):分区少、流量集中,你很早就限流。
  • 高基数deviceIduserId、一个订单 ID):许多值哈希到许多分区,负载摊开,余量增长。

从 SQL 过来,你会乐呵呵地给一个 status 列建索引并在它上面过滤。但作为 DynamoDB 的分区键,那是个陷阱——它摊不开。把低基数的属性留作过滤条件,或作为二级索引的排序键,永远别把它当作决定放置位置的东西。

当一个天然不错的键仍然倾斜时——一小撮巨鲸租户把其余的甩在后头——就加一个后缀,把一个逻辑值扇出到 N 个分区上,例如给分片写入路径用 tenantId#3。你在读取时再重新聚合。

一旦你的键摊开,要在一个分区_内部_定位项目,你就要写一个作用于排序键的 KeyConditionExpression。在把它接进代码之前,你可以在 DynamoDB 表达式构建器里针对你自己的 schema 组装一个:

deviceId = "vac-7741" AND readingTs BETWEEN "2026-06-01" AND "2026-06-30"

那会从单个分区读取一台设备的六月窗口——是一次 Query,不是 Scan。分区键钉住机器;排序键条件收窄行。

陷阱与后续步骤

  • 别按在 SQL 里读起来顺手来挑键。 按什么能_摊开_来挑。基数第一,查询便利第二。
  • 别以为表的总容量在每个键上都归你用。 吞吐是按分区来的;单个热值就能限流,而表看着却闲着。
  • 别跟拆分较劲。 它是自动的、由哈希驱动的——你的活是给它足够多不同的键去摊开。

一旦你的键干净地摊开,接下来的决定就是如何在一个分区内布局项目——见单表设计——以及何时一个二级索引才是应对第二种访问模式的对的工具。

下载 DynoTable,在 SQL Workbench 里对你的分区键跑一个 GROUP BY,看看在某个键变成热分区之前,哪些键在堆积项目。

更新于