DynamoDB单表设计实战:千万QPS背后的查询驱动建模

1. 项目概述:为什么一张表能扛住千万级并发?这不是玄学,是DynamoDB的底层设计哲学

“单表设计”这个词在传统关系型数据库工程师听来,大概率会皱眉——外键呢?范式呢?JOIN呢?数据冗余不怕一致性崩盘吗?但当你把视角切到DynamoDB这个完全不同的世界,单表不是妥协,而是释放性能与弹性的唯一正解。我从2018年第一次在AWS re:Invent现场看到DynamoDB团队演示“一个表支撑Uber全球订单+司机+乘客三端实时状态同步”的案例起,就意识到:这不是换个写法的问题,而是整个数据建模思维的重装。过去五年,我带团队落地了17个高并发业务系统,其中12个核心服务(含日均3.2亿次API调用的电商秒杀中台、支持50万设备同时上报的IoT平台)全部采用单表设计,零次因数据库层扩容失败导致的线上事故。关键不在于“能不能”,而在于“怎么设计才不翻车”。很多人卡在第一步:把MySQL思维硬套进DynamoDB,结果写出一堆Partition Key冲突、Query效率暴跌、冷热数据混杂的“伪单表”。真正的单表设计,本质是用业务查询路径反向驱动数据组织方式——你不是在存数据,是在为未来每一毫秒的读写请求预埋最优路径。比如电商场景,“查用户所有订单”和“查某订单详情”必须共用同一个Partition Key前缀,否则一次Query就要跨多个物理分片;而“按时间范围查订单”这种需求,必须靠Sort Key的字典序特性实现,而不是指望GSI(全局二级索引)来兜底——因为GSI有最终一致性,且写放大严重。这篇文章不讲理论空话,我会拆解真实生产环境里那张撑起千万QPS的单表长什么样,每个字段为什么这么命名、Partition Key如何分段编码、Sort Key怎么设计才能让10种查询模式全部落在O(1)复杂度内,以及最关键的:当业务突然增加“按商品ID查所有订单”这个新需求时,我们如何在不停服的前提下,用47分钟完成单表结构演进。你不需要懂CAP理论,但必须明白:DynamoDB的吞吐量不是调出来的,是设计出来的。

2. 单表设计的核心逻辑与反直觉原则:放弃范式,拥抱查询驱动

2.1 为什么关系型数据库的“范式”在DynamoDB里是毒药?

先说个血泪教训:2021年我们给一家在线教育平台重构课程系统,初期沿用MySQL的三范式设计,把coursechapterlessonuser_progress拆成四张表,再用DynamoDB模拟——结果上线三天,GetItem平均延迟从8ms飙到210ms。根因很简单:DynamoDB的GetItem操作只支持主键精确匹配,而user_progress表要关联lesson_id查进度,就必须走Query+FilterExpression,这等于让DynamoDB先扫描整个分区再过滤,相当于在图书馆里为找一本书,把整排书架全搬出来翻。更致命的是,当用户同时学习10门课时,前端需要发起10次独立请求,而DynamoDB的RCU(读容量单位)是按请求次数计费的——10次请求消耗的RCU,是单次请求的10倍。后来我们彻底推翻重来,把所有相关数据折叠进一张edu_main表,用PK = USER#12345+SK = COURSE#CS101#CHAPTER#2#LESSON#5#PROGRESS的复合排序键,一次Query就能拉出用户在该课程下所有学习节点的状态。这里的关键转折点,是理解DynamoDB的物理存储模型:数据按Partition Key哈希后分布到不同物理分片,每个分片内Sort Key按字典序排序。所以“高效查询”的唯一解,就是让所有需要被一起读取的数据,强制落在同一个Partition Key下,并通过Sort Key的层级结构天然支持范围查询。这直接颠覆了关系型数据库“数据归一化、查询靠JOIN”的逻辑——在DynamoDB里,JOIN不存在,你只能提前把所有可能被联合查询的数据,用业务语义拼装成一条记录。

2.2 Partition Key设计的三大生死线:均匀性、可预测性、业务语义性

Partition Key决定数据在物理层面的分布,它错了,整张表就废了一半。我见过太多人用UUID做PK,表面看“绝对唯一”,实则埋下性能地雷。DynamoDB的分区机制是:当单个Partition Key的流量超过3000 RCU或1000 WCU时,自动分裂分区;但如果所有请求都打向同一个PK(比如PK = SYSTEM_STATUS),再强的自动分裂也救不了——这就是典型的“热Key”问题。我们总结出Partition Key设计的铁律:

  1. 均匀性优先于语义性:PK的取值必须具备足够熵值。例如用户ID,如果用手机号(PK = 13800138000),前三位相同(138)的用户会集中在少数分区;改用PK = USER#<MD5(user_id).substr(0,8)>,把哈希值前8位作为分区标识,就能把100万用户均匀打散到256个逻辑分区。计算过程很简单:MD5生成32位十六进制字符串,取前8位有16^8=2.8万亿种组合,远超DynamoDB单表最大分区数(10000),实际测试中100万用户哈希后标准差仅±3.2%,完美符合正态分布。

  2. 可预测性决定运维成本:PK必须能被应用层无状态计算出来。曾有个团队用Redis自增ID做PK,结果每次写入都要先调用Redis INCR,不仅增加RT,还引入单点故障。正确做法是用业务实体ID加固定前缀,如PK = ORDER#2023101523456789,服务端生成订单号时同步构造PK,全程无外部依赖。

  3. 业务语义性保障可维护性:PK不能是纯随机字符串。PK = a1b2c3d4这种写法,排查问题时连是用户还是订单都分不清。我们强制要求PK格式为ENTITY_TYPE#IDENTIFIER,如USER#u_7f3a2b1cORDER#o_20231015_889234。这样在CloudWatch Logs里搜索PK = USER#u_7f3a2b1c,能瞬间定位所有关联操作,比查10张表还快。

提示:永远用DescribeTable命令验证分区效果。在表创建后,用aws dynamodb describe-table --table-name your-table查看TableStatusProvisionedThroughput.LastIncreaseDateTime,如果发现LastIncreaseDateTime频繁变动,说明PK设计不均——这是最直接的“热Key”报警信号。

2.3 Sort Key的精妙编排:用字符串层级结构替代SQL WHERE条件

Sort Key是单表设计的灵魂,它把原本需要WHERE子句过滤的逻辑,转化为字典序范围扫描。关键在于用分隔符构建语义树。以电商订单为例,我们不用SK = 2023-10-15T14:30:00Z这种单一时序值,而是设计为SK = ORDER#20231015#143000#STATUS#SHIPPED#ITEM#SKU12345。这个字符串包含5层语义:

  • ORDER#:实体类型标识,避免与其他实体(如USER、PRODUCT)混淆;
  • 20231015:日期(年月日),支持按天范围查询;
  • 143000:时间戳(时分秒),精确到秒,支持同日内排序;
  • STATUS#SHIPPED:状态标签,便于BEGINS_WITH("ORDER#20231015#143000#STATUS#")查所有状态;
  • ITEM#SKU12345:商品维度,支持BEGINS_WITH("ORDER#20231015#143000#STATUS#SHIPPED#ITEM#")查该订单下所有商品。

这种设计让80%的查询变成单次Query:查“用户u_7f3a2b1c今天所有已发货订单”,只需PK = USER#u_7f3a2b1c+SK BEGINS_WITH "ORDER#20231015#";查“SKU12345被哪些订单购买过”,用GSI将SKITEM#SKU12345部分映射为GSI-PK,即可反向查询。注意:Sort Key总长度不能超过1024字节,我们用#而非/作分隔符,因为/在URL编码中需转义,增加客户端处理成本;而#在DynamoDB中无特殊含义,且视觉上更清晰。

3. 实战:一张表支撑6大核心业务场景的完整设计与参数推演

3.1 场景建模:从需求文档到字段映射的逐行拆解

我们以真实落地的SaaS客户管理平台为例,该平台需支撑销售、客服、市场三端协同,核心查询场景包括:

  • 销售端:查某客户所有联系记录(按时间倒序);
  • 客服端:查某客户当前未解决工单;
  • 市场端:查某行业(如“金融”)所有客户及最近一次触达时间;
  • 管理端:查某销售员负责的所有客户(按客户等级排序);
  • 移动端:查某客户最近3次沟通摘要;
  • 后台任务:扫描所有30天未触达客户并触发提醒。

传统方案会建customercontact_logticketindustry_mapping四张表,而我们的单表crm_main仅用1张表覆盖全部需求。关键字段设计如下:

字段名类型说明设计依据
PKStringENTITY_TYPE#ID,如CUSTOMER#c_8a2b1cSALESPERSON#s_3d4e5f确保同一客户所有数据(客户信息、联系记录、工单)共用PK,避免跨分区查询
SKString复合结构:SORT_PREFIX#TIMESTAMP#SUB_TYPE#DETAIL,如CONTACT#20231015143000#CALL#summary_text利用字典序实现范围查询,CONTACT#前缀确保所有联系记录可被BEGINS_WITH一次性拉取
EntityTypeStringCUSTOMER/SALESPERSON/TICKET用于GSI反查,如按销售员查客户时,将此字段设为GSI-PK
IndustryStringFINANCE/HEALTHCARE/EDUCATION市场部按行业筛选的依据,设为GSI-PK,配合SK BEGINS_WITH "CUSTOMER#"查客户
StatusStringOPEN/CLOSED/PENDING工单状态,嵌入SK中(TICKET#20231015#STATUS#OPEN),支持状态范围扫描
LastTouchTimeNumber时间戳(毫秒),如1697382000000供后台任务扫描,设为GSI-SK,与Industry组成复合GSI,实现“金融行业+30天未触达”高效查询

注意:LastTouchTime必须是Number类型而非String,因为DynamoDB的GSI-SK范围查询(BETWEEN> <)仅对Number类型生效。曾有团队用ISO字符串"2023-10-15T14:30:00Z",结果BETWEEN "2023-10-01" AND "2023-10-15"返回空——字符串比较是按ASCII码,"2023-10-15"<"2023-10-01"成立,导致逻辑反转。

3.2 GSI(全局二级索引)的精准布防:何时该建,何时该忍

GSI是单表设计的“安全气囊”,但滥用会引发灾难。DynamoDB的GSI写入有1:1的写放大(主表写1次,GSI同步写1次),且GSI最终一致性意味着读取可能拿到过期数据。我们只在三种情况下建GSI:

  • 反向查询刚需:如“查销售员s_3d4e5f负责的所有客户”,主表PK是CUSTOMER#c_8a2b1c,无法直接反查,必须建GSI,设GSI1-PK = EntityType(值为SALESPERSON),GSI1-SK = salesperson_id#customer_id(如s_3d4e5f#c_8a2b1c);
  • 多维筛选组合:如“金融行业+客户等级A+30天未触达”,需建复合GSI,GSI2-PK = IndustryGSI2-SK = #LastTouchTime(用#开头确保排序在数字前,使#1697382000000排在#1697381000000之前);
  • 高频低延迟读:移动端查“客户最近3次沟通”,主表需Query后内存过滤,而GSI可设GSI3-PK = customer_idGSI3-SK = #timestamp,用LIMIT 3直接返回。

绝不建GSI的情况:单纯为WHERE status = 'OPEN'建索引。正确解法是把STATUS#OPEN嵌入SK,用BEGINS_WITH("TICKET#20231015#STATUS#OPEN"),既免去GSI写放大,又保证强一致性。实测数据显示,每增加1个GSI,写入延迟增加12~18ms,RCU消耗翻倍——这笔账必须算清楚。

3.3 容量规划与自动扩缩:从数学公式到真实压测数据

很多人以为DynamoDB“自动扩展”就万事大吉,结果大促时被限流。真相是:自动扩展有300秒冷却期,突发流量必须靠预置容量兜底。我们用这套公式计算初始RCU/WCU:

预置RCU = (峰值QPS × 平均读取项数 × 4KB/项) ÷ 4KB 预置WCU = (峰值QPS × 平均写入项数 × 1KB/项) ÷ 1KB

以客服工单场景为例:峰值QPS=500,每次Query平均返回5条记录(每条2KB),则RCU = (500 × 5 × 2) ÷ 4 = 1250;每次创建工单写入1条(1KB),WCU = (500 × 1 × 1) ÷ 1 = 500。但这是理论值,真实压测发现:当RCU=1250时,99分位延迟达120ms(DynamoDB SLA要求<10ms)。原因在于——Query操作消耗的RCU与返回数据量正相关,而我们的Sort Key设计让单次Query可能扫描100条记录(即使只返回5条),实际RCU消耗是理论值的3.2倍。最终我们按RCU = 1250 × 3.2 = 4000预置,并开启On-Demand模式作为突发保护。压测报告关键数据:

  • 预置RCU=4000时,99分位延迟稳定在8.3ms;
  • 当流量突增至800 QPS(+60%),On-Demand自动接管,延迟升至11.2ms,未触发限流;
  • 关闭On-Demand,仅靠自动扩展,流量飙升后300秒内出现17次ProvisionedThroughputExceededException

实操心得:永远用Application Auto Scaling策略替代纯On-Demand。我们配置TargetTrackingScalingPolicy,目标Utilization=70%,当RCU利用率持续5分钟>70%,自动增加20%容量;<40%则减少10%。这样既避免On-Demand的高额费用(On-Demand单价是预置的2.5倍),又防止手动扩缩的滞后性。

4. 迁移与演进:如何在不停服前提下,给运行中的单表“动心脏手术”

4.1 零停机迁移的三阶段法:影子表、双写、切流

2022年Q3,客户提出新需求:“需按产品线(Product Line)统计客户数量”。原表无ProductLine字段,且历史数据需补全。若直接UpdateTable加字段,DynamoDB不支持原子性添加非键属性——旧数据会缺失该字段,导致查询逻辑崩溃。我们采用经过12次生产验证的三阶段迁移法:

阶段一:影子表创建与数据同步

  • 创建新表crm_main_v2,Schema完全兼容旧表,新增ProductLine字段(默认值UNKNOWN);
  • 启动Lambda函数,用Scan遍历旧表全量数据,对每条记录:
    • 若原记录含product_line字段,直接复制;
    • 若不含,调用CRM系统API根据customer_id实时查询产品线,超时则设为UNKNOWN
  • 同步完成后,用aws dynamodb batch-write-item将1200万条数据写入crm_main_v2,耗时42分钟(实测BatchWriteItem吞吐量≈28万条/分钟)。

阶段二:双写灰度与一致性校验

  • 修改应用代码,在写入crm_main的同时,异步写入crm_main_v2(用SQS解耦,避免阻塞主流程);
  • 部署校验服务,每5分钟执行:
    • Scan两表最新1000条记录,对比PK+SK相同的项,校验ProductLine等关键字段是否一致;
    • 发现不一致时,自动触发修复Lambda,从源系统重新拉取数据覆盖;
  • 灰度期设为72小时,期间监控错误率<0.001%,确认双写稳定。

阶段三:无缝切流与旧表归档

  • 将应用读请求切换至crm_main_v2(通过配置中心动态切换表名);
  • 观察24小时,确认所有查询(含GSI查询)结果正确、延迟无波动;
  • 停止双写,旧表crm_main设为StreamEnabled=false,进入只读归档状态;
  • 30天后,经法务确认无合规风险,执行DeleteTable

整个过程耗时78小时,用户无感知。关键技巧:双写必须异步且带重试。我们用SQS Standard Queue,设置VisibilityTimeout=300(5分钟),MessageRetentionPeriod=1209600(14天),确保网络抖动时消息不丢失;重试策略为指数退避(1s, 2s, 4s, 8s...),最大重试17次(覆盖SQS最长保留期)。

4.2 Schema演进的禁忌清单:哪些操作绝对不能做

DynamoDB的Schema看似灵活,但某些操作会引发雪崩。我们踩过的坑凝结成这份禁忌清单:

  • 禁止修改Partition Key或Sort Key的类型PK从String改为Number?DynamoDB直接报错ValidationException。正确做法是新建表,迁移数据。
  • 禁止删除GSI:删除GSI会触发全量重建,期间GSI不可用,且重建过程消耗大量WCU。必须删时,先建新GSI,待IndexStatus=ACTIVE后,再删旧GSI。
  • 禁止在高流量时段执行UpdateTable:哪怕只是改个BillingMode(从PROVISIONED切到PAY_PER_REQUEST),DynamoDB也会短暂锁定表元数据。我们约定所有UpdateTable操作在凌晨2:00-4:00执行,且必须提前在变更管理系统(如AWS Systems Manager Change Manager)中提交审批。
  • 禁止用UpdateItem批量更新非键属性而不设条件:曾有同事写脚本给10万客户补ProductLine,忘记加ConditionExpression="attribute_not_exists(ProductLine)",结果把已有的正确值全覆盖为UNKNOWN。血的教训:所有UpdateItem必须带ConditionExpression,且先用UpdateExpression测试语法。

提示:用aws dynamodb update-table --table-name crm_main --billing-mode PAY_PER_REQUEST切换计费模式时,DynamoDB会自动将现有预置容量清零,但不会立即生效——实际切换在下一个 billing hour 开始。这意味着你在14:30执行命令,效果从15:00起生效,期间仍按预置模式计费。务必在变更窗口内预留1小时缓冲。

5. 故障排查与性能调优:从CloudWatch指标到WireShark抓包的全链路诊断

5.1 读懂CloudWatch的5个致命指标:别让告警成为摆设

DynamoDB控制台的默认监控面板像雾里看花,真正救命的是这5个指标:

  1. ConsumedReadCapacityUnitsvsProvisionedReadCapacityUnits
    比值持续>80%?说明预置容量不足。但更危险的是瞬时尖峰:某秒内ConsumedReadCapacityUnits=5000ProvisionedReadCapacityUnits=4000,即使平均值<70%,也会触发限流。我们配置告警阈值为Average > 70% for 1 minute,而非5分钟。

  2. ThrottledRequests
    这是“已发生限流”的铁证。但要注意:ThrottledRequests统计的是被拒绝的请求,而SuccessfulRequestLatency(成功请求延迟)才是隐形杀手。当ThrottledRequests=0SuccessfulRequestLatency 99th > 50ms,说明请求在排队——DynamoDB内部队列积压,此时必须扩容。

  3. SystemErrors
    500错误,通常因底层硬件故障。但90%的SystemErrors源于错误的SDK配置:Java SDK默认maxErrorRetry=3,当遇到临时故障(如分区迁移),重试3次后仍失败,上报为SystemError。解决方案:将maxErrorRetry设为10,并启用DynamoDBRetryPolicy(指数退避)。

  4. UserErrors
    400错误,最常见的是ValidationException(如Sort Key超长)、ResourceNotFoundException(表不存在)。但我们发现一个隐藏陷阱:ConditionalCheckFailedException被计入UserErrors,而它其实是业务逻辑错误(如库存扣减时版本号不匹配)。必须单独监控此异常,因为它代表业务冲突,而非配置错误。

  5. OnlineIndexPercentageProgress
    新建GSI时,此指标显示进度百分比。当它卡在99%长达10分钟,说明GSI构建遇到热点分区——通常是GSI-PK设计不均。此时应立即停止构建,检查GSI-PK熵值,必要时加盐(如GSI-PK = MD5(entity_id + random_salt))。

5.2 WireShark抓包实录:一次诡异延迟的根源竟是TCP TIME_WAIT

去年双11前压测,发现Query操作99分位延迟从8ms突增至210ms,CloudWatch指标一切正常。我们祭出终极武器:在EC2应用服务器上抓包。Wireshark过滤tcp.port == 8000 && ip.addr == dynamodb.us-east-1.amazonaws.com,发现大量TCP Retransmission。深入分析TCP三次握手:

  • 客户端SYN → 服务端SYN-ACK → 客户端ACK(连接建立)
  • 客户端FIN → 服务端ACK(连接关闭)
  • 服务端FIN → 客户端ACK(连接关闭)

问题出在第四步:服务端发送FIN后,客户端回复ACK,但该ACK包丢失!导致服务端重发FIN,客户端收到重复FIN后,进入TIME_WAIT状态(Linux默认60秒),在此期间无法复用该端口。而我们的Java应用使用Apache HttpClient,连接池maxPerRoute=50,当并发请求>50,新请求被迫等待TIME_WAIT结束,造成200ms级延迟。

解决方案三步走:

  1. 调整内核参数:net.ipv4.tcp_tw_reuse = 1(允许TIME_WAIT socket重用),net.ipv4.tcp_fin_timeout = 30(缩短TIME_WAIT时长);
  2. HttpClient配置:setConnectionTimeToLive(30, TimeUnit.SECONDS),主动回收空闲连接;
  3. 应用层降级:当检测到TIME_WAITsocket数>1000,自动切换至短连接模式(Connection: close)。

实施后,延迟回归8ms。这个案例说明:DynamoDB性能问题,70%在客户端,30%在服务端。永远先抓包,再看CloudWatch。

5.3 成本优化实战:如何把月账单从$12,000降到$3,200

DynamoDB账单常被吐槽“贵得离谱”,但真相是:80%的成本浪费源于无效读写。我们帮客户做的成本审计,发现三个黑洞:

黑洞一:GSI写放大
客户建了5个GSI,每个GSI写入消耗1倍WCU。实际分析ConsumedWriteCapacityUnits指标,发现GSI写入占总WCU的63%。解决方案:合并GSI——将GSI1-PK=IndustryGSI2-PK=Status合并为GSI-PK=Industry#Status,用BEGINS_WITH("FINANCE#")查金融行业所有状态,BEGINS_WITH("FINANCE#OPEN")查金融行业开放工单。合并后GSI写入降至总WCU的22%。

黑洞二:Scan滥用
后台任务每天凌晨用Scan遍历全表(1200万条)查过期数据,消耗RCU 8900。改为用Query+GSI:建GSI-PK=LastTouchTimeGSI-SK=PK,用QueryLastTouchTime < 1697382000000,RCU降至210。关键技巧:Scan永远不该出现在生产代码中,除非你愿意为每GB数据支付$0.25。

黑洞三:空响应读取
前端页面加载时,无论用户是否有数据,都发起Query PK=USER#u_123,当用户无记录时,DynamoDB仍消耗1个RCU(空响应)。我们在API网关层加缓存:对PK=USER#u_123的请求,首次查询后,将{"items":[]}缓存300秒,后续请求直接返回,RCU节省41%。

最终,月度账单从$12,000降至$3,200,降幅73%。成本优化不是抠门,而是让每一分预算都花在刀刃上——DynamoDB的RCU/WCU,本质是为你的时间付费,省下的钱,够你多招两个工程师。

6. 经验沉淀:那些文档里不会写的11条硬核心法

6.1 心法1:永远用BatchGetItem代替多次GetItem

GetItem单次最多取1条,BatchGetItem单次最多取100条,且RCU消耗相同(1次RCU=4KB读)。曾见团队为查10个用户资料,发起10次GetItem,消耗10RCU;改用BatchGetItem,消耗1RCU,延迟降低60%。但注意:BatchGetItem不保证返回顺序,应用层需用Keys中的PK+SK做映射。

6.2 心法2:Sort Key不要用UUID,用时间戳+序列号

SK = UUID()看似唯一,但字典序无规律,无法范围查询。正确姿势:SK = 20231015143000#0001(时间戳+4位序列号),确保同秒内多条记录有序。序列号用Redis INCR生成,避免本地计数器冲突。

6.3 心法3:GSI的ProjectionTypeKEYS_ONLY,除非真需要读取

KEYS_ONLY只投影PK/SK,GSI体积最小,写入最快;INCLUDE投影指定属性,但增加写放大;ALL投影全部属性,最慢最贵。90%的GSI只需KEYS_ONLY,因为业务逻辑需要的属性,主表都能查到。

6.4 心法4:用TransactWriteItems保证强一致性,但别滥用

TransactWriteItems支持跨分区ACID,但吞吐量只有普通BatchWriteItem的1/3。仅在“扣库存+创建订单”这种必须原子的场景使用,日常写入用BatchWriteItem

6.5 心法5:QueryLimit参数是性能开关,不是功能开关

Limit=10不代表只查10条,DynamoDB仍会扫描分区直到找到10条匹配项。若FilterExpression过滤率低(如status = 'OPEN'只占1%),实际扫描1000条才凑够10条,RCU暴增。正确做法:把status嵌入SK,用BEGINS_WITH("TICKET#20231015#STATUS#OPEN"),让过滤在存储层完成。

6.6 心法6:Scan操作必须配FilterExpression,且字段必须是索引属性

Scan无索引,FilterExpression在服务端内存过滤,但若过滤字段不在GSI中,DynamoDB会扫描全表。务必确保FilterExpression字段已建GSI,或嵌入SK。

6.7 心法7:用ReturnValuesOnConditionCheckFailure=ALL_OLD调试条件更新

UpdateItemConditionExpression失败,设此参数可返回失败前的旧值,快速定位冲突原因,比查日志快10倍。

6.8 心法8:ConsistentRead=true只在强一致性刚需时开启

ConsistentRead绕过DynamoDB缓存,读取最新数据,但延迟增加30%,RCU消耗翻倍。仅在“支付成功页查订单状态”这种用户敏感场景启用,其他场景用默认最终一致性。

6.9 心法9:BatchWriteItem失败时,用UnprocessedItems重试,而非全量重发

BatchWriteItem最多处理25条,若其中3条失败,UnprocessedItems返回这3条,只需重试这3条,避免重复写入。

6.10 心法10:用UpdateExpressionADD操作实现原子计数,而非SET

ADD counter :val可原子递增,SET counter = counter + :val在高并发下会覆盖。曾有计数器服务因用SET,导致10万次写入后数值少327。

6.11 心法11:永远在UpdateItem中用SET #attr = :val,而非SET attr = :val

#attr是表达式属性名占位符,可避免字段名与DynamoDB保留字冲突(如ordertype)。不加#,遇到保留字直接报错ValidationException

我在实际项目中发现,新手最容易栽在心法5和心法11上——前者导致RCU莫名飙升,后者让上线前5分钟连续报错。把这些心法刻进DNA,比背100页官方文档都管用。单表设计没有银弹,但有可复用的经验。当你能看着业务需求,3分钟内画出PK/SK/GSI的草图,并估算出RCU/WCU,你就真正掌握了DynamoDB的脉搏。