2 分钟

Amazon DynamoDB 详解:构建可扩展系统

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

Amazon DynamoDB 详解:构建可扩展系统

什么是 DynamoDB,以及为什么团队选择它

Amazon DynamoDB 是 AWS 提供的完全托管 NoSQL 数据库服务,面向需要在几乎任意规模下保持一致低延迟读写的应用。“完全托管”意味着 AWS 负责基础设施工作——硬件配置、复制、打补丁和许多运维任务——因此团队可以把精力放在交付功能而不是运行数据库服务器上。

DynamoDB 的核心是将数据以item(行)存储在table 中,但每个 item 可以有灵活的属性。其数据模型可被理解为键值与文档模型的混合:

  • 键值(Key-value):通过主键快速获取 item,就像按 ID 查记录。
  • 文档(Document):存储嵌套属性(map 和 list),类似 JSON,适合相关字段但不需要严格模式的场景。

当团队希望获得可预测的性能并简化运维,且工作负载不适合关系型连接(joins)时,通常会选择 DynamoDB。常见用途包括微服务(各服务拥有自己的数据)、流量突发的无服务器应用,以及对数据变化做出反应的事件驱动系统。

本文将介绍构建块(表、键、索引)、如何围绕访问模式建模(包括单表设计)、扩展与容量模式如何工作,以及把变更流式化进事件驱动架构的实践模式。

核心概念:表、项和主键

DynamoDB 围绕一些简单的构建块组织,但这些细节决定了你如何建模数据以及请求的速度(和成本)。

表、item 与属性

表(table) 是顶层容器。表中的每条记录是一个 item(类似一行),每个 item 是若干 attributes(类似列)。

与关系型数据库不同,同一表内的 items 不需要共享相同的属性。一个 item 可能有 {status, total, customerId},另一个可能包含 {status, shipmentTracking}——DynamoDB 不要求固定模式。

主键:简单与复合

每个 item 都由一个 主键 唯一标识,DynamoDB 支持两种类型:

  • 简单主键(仅分区键):单一属性唯一标识每个 item。
  • 复合主键(分区键 + 排序键):多个 item 可以共享同一分区键,而通过 排序键 区分并在该分区内定义顺序。

在实践中,复合键支持按组访问的模式,比如“某个客户的所有订单,按时间新到旧”。

Query 与 Scan(高层)

Query主键(或索引键)读取 item。它针对特定分区键,并能按排序键范围过滤——这是高效且首选的路径。

Scan 则会遍历整个表(或索引),然后再过滤。它容易上手,但在规模化时通常更慢且更昂贵。

需要注意的限制

一些早期会遇到的约束:

  • 最大 item 大小:400 KB。
  • 属性类型:标量(字符串/数字/二进制/布尔/空)、集合、列表和映射。
  • 键属性必须是标量(不能用列表/映射作为分区键或排序键)。

这些基础会影响后面的访问模式、索引选择以及性能特性。

DynamoDB 的数据模型:键值与文档

DynamoDB 常被描述为既是键值存储又是文档数据库。这是准确的,但理解各自含义对日常设计很有帮助。

键值访问 vs 文档式 item

核心是通过键检索数据。给出主键值,DynamoDB 返回单个 item。这种键控查找为许多工作负载带来可预测的低延迟存储。

同时,item 可以包含嵌套属性(maps 和 lists),使其具有文档数据库的感觉:你可以存储结构化负载,而无需提前定义严格模式。

在 item 中建模层级化的 JSON 结构

Items 自然对应类 JSON 的数据:

  • Maps 表示对象(例如 profile.name, profile.address)。
  • Lists 表示数组(例如最近操作、标签)。

当一个实体通常作为整体被读取时(如用户资料、购物车或配置包),这种方式非常合适。

何时去规范化(denormalize)以及为何常见

DynamoDB 不支持服务端连接(joins)。如果应用需要在一个读取路径中获取“订单及其行项和运输状态”,通常会选择去规范化:把某些属性复制到多个 item,或将小的子结构直接嵌入到 item 中。

与关系型规范化的权衡

去规范化会增加写入复杂性,可能导致更新扇出(update fan-out)。其回报是减少往返请求和加速读取——这在可扩展系统中往往是关键路径。

分区键与排序键:为访问模式而设计

最快的 DynamoDB 查询是你能表达为“给我这个分区”(可选地“在该分区内给我这个范围”)。因此,键的选择更多是关于如何读取数据,而不仅是如何存储它。

分区键:分布与可预测读取

分区键 决定了 item 存放在哪个物理分区。DynamoDB 对该值做哈希以分散数据与流量。如果大量请求集中在少数分区键值上,会产生“热”分区并触及吞吐限制,即便表大部分时间空闲。

良好的分区键应:

  • 具有 高基数(很多不同的值)
  • 高频访问模式 匹配(使读取直接而非过滤)
  • 避免成为“热门”值(例如常量 "GLOBAL"

排序键:范围查询与分组实体

有了 排序键,共享同一分区键的 items 会被存放在一起并按排序键排序。这使得以下操作高效:

  • 范围查询BETWEENbegins_with
  • 按时间排序读取(用反向扫描实现最新优先)
  • 实体分组(在一个分区下存放多种 item 类型)

常见做法是组合排序键,例如 TYPE#idTS#2025-12-22T10:00:00Z,以支持多种查询形态而不需额外表。

将常见访问模式映射到键

  • 按 ID 获取PK = USER#<id>(简单的 GetItem
  • 按用户列出PK = USER#<id>SK begins_with ORDER#(或 SK = CREATED_AT#...
  • 时间序列范围PK = DEVICE#<id>SK = TS#<timestamp>,用 BETWEEN 查询时间窗口

键选择如何影响性能与可扩展性

如果你的分区键与最高流量的查询对齐并且能均匀分布,你将获得稳定的低延迟读写。否则,你将通过扫描、过滤或额外索引来补偿——这些都会增加成本并提高出现“热键”的风险。

二级索引:GSI 与 LSI 说明

二级索引为 DynamoDB 提供除了表主键之外的备用查询路径。当新的访问模式出现而不适用于原始键设计时,可以添加索引来为相同 items 重新键值以支持不同查询。

GSI 与 LSI:有什么区别?

全局二级索引(GSI) 有自己的分区键(可选排序键),可以与主表完全不同。它是“全局的”,跨表的所有分区,且可以随时添加或删除。需要新的访问路径(例如按 customerId 查询订单,而表以 orderId 为键)时使用 GSI。

本地二级索引(LSI) 与基表共享相同的分区键但使用不同的排序键。LSI 必须在建表时定义。它们适用于在同一实体组内需要多种排序顺序的情形(比如按 createdAt 与按 status 两种顺序获取客户订单)。

投影:哪些属性会被复制到索引

投影决定 DynamoDB 在索引中存储哪些属性:

  • KEYS_ONLY:最省存储,但通常需要额外的基表读取。
  • INCLUDE:只复制常用返回的属性。
  • ALL:最简单,但会增加存储与写入成本。

写放大(隐藏的账单)

对基表的每次写入可能触发写入到一个或多个索引。更多的 GSI 和更宽的投影会增加写成本和容量消耗。围绕稳定的访问模式规划索引,并尽可能把投影属性保持最小。

容量模式与扩展行为

围绕 Streams 事件设计
草拟事件驱动流程,并为变更事件配置处理器,后端由聊天生成。

DynamoDB 的扩展从一个选择开始:按需(On-Demand)预配置(Provisioned)。两者都能达到很高吞吐量,但在变化流量下表现不同。

按需 vs 预配置:如何选择

按需 最简单:按请求付费,DynamoDB 会自动适应可变负载。适合流量不可预测、处于早期或有突发负载的场景,不想管理容量目标的团队。

预配置 则是容量规划:你指定(或自动扩缩)读写吞吐量,在稳定使用下通常能获得更可预测的成本。适用于能预测需求的工作负载。

读/写容量的实际含义

预配置吞吐以:

  • RCU(Read Capacity Units) 衡量:大约一个强一致读取每秒可读至多 4 KB(或两个最终一致读取)。
  • WCU(Write Capacity Units) 衡量:大约一个写入每秒可写至多 1 KB。

实际成本受 item 大小、一致性要求和是否扫描等访问模式影响:较大的 item、强一致与扫描会快速消耗容量。

自动伸缩基础(及限制)

自动伸缩会基于利用率目标调整预配置的 RCU/WCU。它有助于逐步增长和周期性负载,但并非瞬时。突然的峰值仍可能被限制,如果容量来不及扩展也无法解决热点分区问题。

DAX:适用于读密集场景的缓存

DynamoDB Accelerator(DAX) 是一个内存缓存,可以降低读取延迟并减轻重复读取的负载(例如热门商品页面、会话查找、排行榜)。它在大量客户端重复请求同一 items 时最有用;对于写密集型模式帮助有限,也不能替代精心的键设计。

一致性、事务与正确性

DynamoDB 允许在读取保证与延迟/成本之间做权衡,因此针对每个操作明确“正确性”含义很重要。

最终一致 vs 强一致读取

默认 GetItemQuery 使用 最终一致 读取:写入后短时间内可能读到旧值。对于资讯流、商品目录等读取多、容忍短暂陈旧的视图通常没问题。

强一致 读取(单区基表可用)保证你能看到最新确认的写入。强一致会消耗更多读取容量并可能增加尾延迟,请仅在真正关键的读取场景使用。

强一致何时重要

强一致适用于决策会触发不可逆动作的读取:

  • 在确认订单前检查可用库存
  • 在授予访问前读取授权标志
  • 在执行下一步之前读取工作流的当前状态

对于计数器,最安全的做法通常不是“强读然后写”,而是使用原子更新(例如 UpdateItemADD),以免丢失增量。

事务性读/写

DynamoDB 事务(TransactWriteItemsTransactGetItems)提供跨最多 25 个 item 的 ACID 语义。适用于必须一起更新多项(如同时创建订单与保留库存)或需要不容许中间态的不变式场景。

幂等性以保证安全重试

分布式系统中重试是正常的。使写入具备幂等性,避免重试导致副作用重复:

  • 使用 客户端请求令牌(幂等键)并与结果一起存储
  • 使用 ConditionExpression 强制唯一性(例如 attribute_not_exists
  • 优先使用原子更新而非读-改-写循环

在 DynamoDB 中,正确性主要关乎选择合适的一致性级别并设计可安全重试的操作。

分区、热键与流量突增

DynamoDB 将表数据分布在多个物理分区中。每个分区对读写吞吐和存储容量都有上限。你的分区键决定了 item 的存放位置;如果太多请求指向同一个分区键值(或一小撮值),该分区就会成为瓶颈。

热分区产生的原因

热分区通常由集中流量的键设计引起:诸如 USER#1TENANT#defaultSTATUS#OPEN 之类的“全局”分区键,或时间序列中所有写入都指向“现在”这一键。

热键和不均匀流量的症状

通常你会看到:

  • 对部分键出现节流(ProvisionedThroughputExceededException
  • 某些访问模式的延迟升高且波动明显,而其他请求仍然很快
  • CloudWatch 指标显示已消费容量不均或突然激增

缓解技术

先为分布设计,然后顾及查询便利性:

  • 键设计:保证分区键高基数(例如用 TENANT#<id> 而不是共享常量)。
  • 写分片:加入小的随机或哈希后缀/前缀,如 ORDER#<id>#<shard> 以分散写入到 N 个 shard,再在需要时跨分片查询。
  • 时间分桶:按小时/天分桶(例如 METRIC#2025-12-22T10)以避免所有写入落到最新一项。

处理突发工作负载

对于不可预测的突发,按需 容量可以在服务限额范围内吸收突发流量。使用 预配置 时,结合自动伸缩并在客户端实现带抖动的指数退避(exponential backoff with jitter)以避免同步重试放大峰值。

可扩展系统的数据建模模式

发布移动配套应用
为你的 DynamoDB 服务构建 Flutter 客户端,快速测试延迟和离线体验。

DynamoDB 的数据建模从 访问模式 出发,而不是 ER 图。你要根据需要快速的查询来设计键,使常见查询成为快速的 Query 操作,其他不常用的查询要么避免,要么以异步方式处理。

单表设计(为什么团队喜欢它)

“单表设计”指在一张表中存放多种实体类型(用户、订单、消息),并使用一致的键约定来在一个 Query 中获取相关数据。这样可以减少跨实体往返并保持延迟可预测。

常见做法是使用复合键

  • PK 分组逻辑分区(例如 USER#123
  • SK 在该组内排序(例如 PROFILEORDER#2025-12-01MSG#000123

这样你可以通过选择排序键前缀来获取“某用户的所有内容”或“仅用户的订单”。

关系:邻接表与多对多

对于图结构关系,邻接表(adjacency list) 很适合:把边作为 item 存储。

  • PK = USER#123, SK = FOLLOWS#USER#456

若需反向查找或真正的多对多,可以添加反向边 item 或将其投影到 GSI,视读取路径而定。

时间序列:分桶 + 排序键 + TTL

对于事件和指标,避免无界分区可以通过分桶实现:

  • PK = DEVICE#9#2025-12-22(设备 + 日期)
  • SK = TS#1734825600(时间戳)

使用 TTL 自动删除过期点,并把聚合(小时/天汇总)作为单独 item 保存以便快速仪表盘查询。

如果想更深入复习键约定,请参阅 /blog/partition-key-and-sort-key-design。

Streams 与事件驱动架构

DynamoDB Streams 是内置的变更数据捕获(CDC)机制。启用表的 Streams 后,每次插入、更新或删除都会产生一个流记录,供下游消费者响应——无需轮询表。

Streams 基础

流记录包含键以及(可选)item 的旧像与新像,取决于你选择的 stream view type(仅键、新镜像、旧镜像或两者)。记录被分组为 shard,需按顺序读取。

构建事件驱动工作流

常见的架构是 DynamoDB Streams → AWS Lambda,每批记录触发一个函数。也可以使用其他消费者(自定义消费者,或将记录推入分析/日志系统)。

典型工作包括:

  • 物化视图:当源表变更时写入去规范化的读模型表。
  • 缓存失效:写入后在 Redis/ElastiCache 中失效或刷新条目。
  • 审计日志:将不可变的变更事件追加到审计表或外部存储。

这将主表保持为低延迟读写的优化目标,并将派生工作推给异步消费者。

有序性、重试与正确性

Streams 在每个 shard 内提供有序处理(通常与分区键相关),但全局没有顺序。投递是至少一次,因此可能产生重复。

为安全处理:

  • 使处理程序 幂等(例如按主键 upsert、使用条件写或记录已处理的事件 ID)。
  • 预期 重试 与部分批次失败;尽可能使用死信队列(DLQ)/失败目标。
  • 将副作用(邮件、支付)放在去重或事务保障之后。

如果按这些保证设计,Streams 能把 DynamoDB 打造为事件驱动系统的可靠骨干。

可靠性、备份与可观测性

DynamoDB 通过在一个区域的多个可用区间分布数据来实现高可用性。对大多数团队来说,实用的可靠性收益来自清晰的备份策略、理解复制选项以及监控适当的指标。

备份:按需与时间点恢复

按需备份 是你在想要已知恢复点时手动(或自动)创建的快照——如在迁移前、发布后或大规模回填前。适合作为“书签”点。

时间点恢复(PITR) 持续捕获更改,以便你能将表恢复到保留窗口内的任意一秒。PITR 是应对误删、错误部署或未被校验的写入的安全网。

复制与多区域选项

若需多区域弹性或接近用户的低延迟读取,Global Tables 可在所选区域之间复制数据。它们简化故障切换,但引入跨区复制延迟与冲突解决的考虑——因此要明确写入模式与 item 的所有权。

监控要点

至少应监控并告警:

  • 读取/写入的延迟(p95/p99)
  • 被节流的请求与系统错误
  • 已消费容量(与预配置容量的余量)

这些信号通常能揭示热分区问题、容量不足或意外的访问模式。

事件演练手册

遇到节流,先定位导致节流的访问模式,然后通过临时切换到按需或增加预配置容量来缓解,并考虑对热点键做分片。

对于部分故障或错误升高,缩小影响范围:禁用非关键流量、使用带抖动的重试并优雅降级(例如提供缓存的读取)直到表恢复稳定。

安全与访问控制

快速为你的 DynamoDB 应用制作原型
通过聊天生成以 DynamoDB 为后端的服务和 UI,并在真实查询出现时优化键。

DynamoDB 的安全主要关乎收紧“谁”能调用哪些 API、从“哪里”调用以及对哪些键有权限。因为表可能包含多种实体(有时是多租户),访问控制应与数据模型一并设计。

IAM 权限:默认最小权限

从基于身份的 IAM 策略开始,限制动作(例如 dynamodb:GetItemQueryPutItem)到最小集合,并将作用域限定到特定表 ARN。

对于更细粒度的控制,使用 dynamodb:LeadingKeys 按分区键值限制访问——当某个服务或租户只能访问其自己的键空间时这非常有用。

加密:需要核实的点

DynamoDB 默认对静态数据进行加密,使用 AWS 管理密钥或客户管理的 KMS 密钥。如果有合规要求,请核实:

  • 表是否配置为使用指定的 KMS 密钥
  • 调用角色是否具有所需的 KMS 权限(且没有多余权限)

对于传输中加密,确保客户端使用 HTTPS(AWS SDK 默认如此)。如果在代理处终止 TLS,请确认代理与 DynamoDB 之间的跳转仍然是加密的。

网络控制:减少外泄路径

使用 VPC Gateway Endpoint 访问 DynamoDB,使流量停留在 AWS 网络内,并能通过端点策略约束访问。配合出站控制(NACL、安全组和路由)以避免“任何流量都能到公网”的路径。

多租户设计与隔离模式

对于共享表,在分区键中包含租户标识(例如 TENANT#<id>),然后用 IAM 条件在 dynamodb:LeadingKeys 上强制租户隔离。

如果需要更强的隔离,请考虑为每个租户或每个环境使用独立表;当运维简便性与成本效率优先于更严格的影响半径时,才采用共享表设计。

成本优化

DynamoDB 常常表现为“当你精确时便宜,当你模糊时昂贵”。账单通常跟随访问模式,因此最好的优化从明确这些模式开始。

了解成本驱动因素

账单主要由:

  • 读取与写入(预配置模式下的 RCU/WCU,按需模式下的请求单位)
  • 存储(表数据及 item 大小)
  • 二级索引(每个 GSI 都有写入与存储成本)
  • Streams(对流记录的读取与下游消费者)

一个常见的惊讶是:对基表的每次写入也会写入到每个受影响的 GSI,因此“再加一个索引”可能会成倍增加写入成本。

通过键设计避免浪费

良好的键设计能减少对昂贵操作的需求。如果你频繁使用 Scan,就是在为扔掉的数据付费。

优先:

  • 分区键Query(可选排序键条件)
  • 在 GSI 中尽量窄投影(只投影真正需要的属性)

如果某个访问模式很少见,考虑用单独表、ETL 作业或去规范化的读模型来服务,而不是永远保留一个 GSI。

通过 TTL 和生命周期控制存储

使用 TTL 自动删除短寿命项(会话、临时令牌、中间工作流状态)。这能削减存储并长期保持索引较小。

对于追加型数据(事件、日志),将 TTL 与能按“仅最近”查询的排序键设计结合,避免经常触及冷历史数据。

右尺寸化容量并避免意外峰值

在预配置模式下,设置保守基线并基于真实指标进行自动伸缩。在按需模式下,关注低效模式(大 item、频繁调用)驱动的请求量。

Scan 当作最后手段——当确实需要全表处理时,应在非高峰时段以分页与退避的受控批处理方式执行。

何时选择 DynamoDB(何时不选)

当你的应用能被表达为一组明确的访问模式,并且需要在高负载下保持一致的低延迟时,DynamoDB 非常出色。如果你能事先描述好读取与写入(按分区键、排序键以及少量索引),它通常是最易于运维的高可用数据存储之一。

适合的场景

DynamoDB 适合:

  • 可预测的查询(获取用户资料、按时间列出用户订单、按 ID 读取会话)
  • 高写入吞吐或不想盯着容量的突发流量
  • 希望水平扩展而不管理服务器
  • 使用 Streams 触发下游工作的事件驱动设计

何时考虑替代方案

若核心需求包括:

  • 跨许多实体的复杂连接或频繁的关系遍历
  • 每周变化的探索性分析与临时查询(group-by、任意过滤)
  • 重度文本搜索与相关性排序而不借助外部索引

则应考虑其他方案。

成功的混合方案

很多团队把 DynamoDB 用于“热”操作读写,然后补充:

  • 用 S3 + Athena 做分析与历史报表
  • 用 OpenSearch(或类似)做全文搜索与分面查询
  • 在某些极热的键上再加缓存层

原型提示:缩短从模型到应用的路径

当你在验证访问模式与单表约定时,速度很重要。团队有时会在 Koder.ai 上快速原型前端与服务,然后随着真实查询出现迭代 DynamoDB 的键设计。即便生产后端不同,快节奏的端到端原型能帮助揭示哪些查询应为 Query,哪些会不小心变成昂贵的 Scan

快速决策清单

验证: (1) 你的核心查询是已知且基于键的,(2) 正确性需求与一致性模型匹配,(3) 预期的 item 大小与增长已被理解,(4) 成本模型(按需 vs 预配置 + 自动伸缩)符合预算。

常见问题

什么是 DynamoDB?什么时候适合使用它?

DynamoDB 是 AWS 提供的完全托管的 NoSQL 数据库,擅长在非常大规模下提供一致的低延迟读写。团队在能以键为中心描述访问模式(按 ID 获取、按拥有者列出、时间范围查询等)并希望避免管理数据库基础设施时,会选择它。

它在微服务(每个服务拥有自己的数据)、无服务器应用以及基于事件的系统中尤为常见。

DynamoDB 中的表、项和属性是什么?

表(table)包含 items(类似行)。每个 item 是一组灵活的 attributes(类似列),可以包含嵌套的数据。

当一次请求通常需要“整个实体”时,DynamoDB 非常适合,因为 item 可以包含 mapslists(类 JSON 结构)。

简单主键和复合主键有什么区别?

仅有 分区键(partition key) 的主键可以唯一标识一个 item(简单主键)。

分区键 + 排序键(sort key) 的组合(复合主键)允许多个 item 共享相同的分区键,同时通过排序键保持唯一并定义顺序。

复合键支持的常见模式包括:

  • “某个客户的所有订单”
  • “某个设备在时间区间内的事件”
什么时候应该使用 Query,什么时候使用 Scan?

Query 在你能指定分区键(可选地加上排序键条件)时使用——这是高效且可扩展的路径。

Scan 则会遍历整个表或索引然后再过滤,通常更慢且更昂贵。频繁扫描通常意味着你的键或索引设计需要调整。

什么是 GSI 和 LSI,我如何选择?

二级索引提供“替代的查询路径”。

  • GSI(全局二级索引):可以使用与主表不同的分区键(和可选排序键);可以在表创建后添加。
  • LSI(本地二级索引):与主表共享分区键但使用不同的排序键;必须在创建表时定义。

索引会增加写入成本,因为写入会被复制到索引中。

如何在按需和预配置容量之间做选择?

如果流量不可预测、突发或你不想管理容量,请选 按需(On-Demand)。按照请求付费。

如果流量稳定且可预测,想要更可控的成本,则选 预配置(Provisioned),并结合自动伸缩。不过它对突发流量的反应不是瞬时的。

DynamoDB 提供哪些一致性选项?何时重要?

默认情况下,读取是 最终一致(eventually consistent),写入之后短时间内可能会读到旧值。

当需要“必须是最新”的检查(例如权限判断、工作流状态)时,使用 强一致(strongly consistent) 读取。对并发正确性,通常优先使用 原子更新(例如 UpdateItemADD 或条件写)而不是先读后写的循环。

什么时候应该使用 DynamoDB 事务?

事务TransactWriteItemsTransactGetItems)在最多 25 个项上提供 ACID 语义。

当必须同时更新多项(例如创建订单并预留库存)或需要不可容忍中间状态的不变式时使用。事务会增加成本并带来额外延迟,因此仅在必要的流程中使用。

什么是热键/热分区?如何避免?

热键/热分区发生在过多请求集中到同一分区键值(或一小撮值)时,即使表整体负载不高,该分区也会被瓶颈化,导致节流。

常见缓解措施包括:

  • 选择高基数的分区键
  • 引入写分片(在键上加小随机/哈希后缀)
  • 对时间序列使用时间分桶
  • 在遇到节流时使用带抖动的指数退避
DynamoDB Streams 如何支持事件驱动架构?

启用 DynamoDB Streams 可以得到写入/更新/删除的变更流。常见模式是 Streams → Lambda 来触发下游工作。

需要注意的保证:

  • 分片内有序(但全局无序)
  • 至少一次投递(可能会有重复)

因此消费者应保证 幂等(按主键 upsert、用条件写或记录已处理的事件 ID)。

Related posts