BatchWriteItem是DynamoDB提供的一个用于批量执行PutItem和DeleteItem操作的API,一次网络往返就能提交多组写入请求。它的核心价值在于减少客户端与服务端之间的往返次数,从而降低网络延迟并提升整体吞吐。然而,这个API并不是无限制地接收请求,它的“批量”二字背后隐藏着几条非常严格的硬性约束。如果你在代码中直接构造一个包含上百个项目的请求,很可能在第一次调用时就收到一个冷冰冰的ValidationException。

BatchWriteItem的三条核心硬性限制
第一条限制是关于写入操作的数量。单个BatchWriteItem请求中,RequestItems这个Map里所有表的所有PutRequest和DeleteRequest加起来,总数不能超过25个。这个“25”是固定的服务端限制,与表的容量模式(按需或预置)无关,也不能通过提工单提高。如果你的业务需要一次性写入100条记录,就必须在客户端自己拆分成4个BatchWriteItem调用,每个调用包含最多25个操作。注意这里的“操作”指的是单条Put或单条Delete,一个PutRequest里如果有多个属性,仍然只算一个操作。
第二条限制是请求的总数据量。整个BatchWriteItem请求的HTTP请求体大小不能超过16MB。这个16MB并不是指你写入DynamoDB后的数据总大小,而是指请求序列化之后的JSON文本大小。每个PutRequest的Item会被转换成DynamoDB的JSON格式,其中每个属性都带有类型描述符(比如{"S":"字符串值"}),因此实际传输的体积会比你直观认为的“业务数据字节数”大不少。举个例子,如果单条记录包含较长的字符串或二进制属性,25条记录加起来很容易就突破16MB,此时虽然操作数只有25个,请求依然会被拒绝。
第三条限制是单条记录的大小。BatchWriteItem中的每个PutRequest对应的项目,其最终存储大小不能超过400KB。这个限制与普通的PutItem操作完全一致。这里说的“存储大小”是指DynamoDB内部计算的项目大小,包括属性名和属性值的长度。如果某个属性的名称很长,或者使用了嵌套的Map和List结构,计算出来的大小会远大于你直觉上的数据量。在实际开发中,很多团队因为把大JSON文档塞进单个属性,导致单条记录超过400KB,进而引发整个BatchWriteItem请求失败。
除了上述三条静态限制,还需要注意写吞吐量的动态限制。如果你的表使用预置容量模式,BatchWriteItem消耗的写容量单位(WCU)是合并计算的。25个Put操作中,每个操作根据项目大小消耗相应的WCU,如果总消耗超过了表的预置写容量,请求不会立即失败,但返回结果中的UnprocessedItems会包含所有未成功写入的操作。此时你需要重新提交这些未处理项,而不是简单地抛异常退出。
UnprocessedItems与重试机制的正确处理方式
BatchWriteItem的返回结果里有一个名为UnprocessedItems的字段。如果因为预置吞吐量不足、内部服务暂时不可用或某种限流导致部分写入没有成功,这些未完成的操作会原样放在UnprocessedItems中返回给你。一个非常常见的错误是开发者只检查HTTP状态码是否为200,看到200就认为全部写入了,实际上可能有一半的操作被静默丢弃了。正确的做法是始终检查返回对象中的UnprocessedItems,只要它不为空,就必须进行重试。
重试不能是一个简单的while循环无脑调用。由于DynamoDB的限流是动态的,立刻重试很可能得到同样的结果。推荐使用指数退避加少量随机抖动。下面给出一段使用AWS SDK for JavaScript v3的完整重试逻辑,它把每个表的未处理项重新组装成新的BatchWriteItem请求,并在每次重试前将延迟时间翻倍,直到所有操作完成或达到最大重试次数。
import { DynamoDBClient, BatchWriteItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ region: "us-east-1" });
async function batchWriteWithRetry(requestItems, maxRetries = 8) {
let currentItems = requestItems;
let delayMs = 100; // 初始延迟100毫秒
for (let attempt = 0; attempt < maxRetries; attempt++) {
const command = new BatchWriteItemCommand({ RequestItems: currentItems });
const response = await client.send(command);
if (!response.UnprocessedItems || Object.keys(response.UnprocessedItems).length === 0) {
return; // 全部写入成功
}
// 有未处理项,准备退避重试
currentItems = response.UnprocessedItems;
const jitter = Math.floor(Math.random() * 50); // 0-49毫秒随机抖动
await new Promise(resolve => setTimeout(resolve, delayMs + jitter));
delayMs = Math.min(delayMs * 2, 5000); // 延迟翻倍,最多5秒
}
throw new Error(`BatchWriteItem在${maxRetries}次重试后仍有未处理项`);
}
这段代码中有两个细节值得注意:第一,每次重试只发送上一次未处理的部分,而不是把原来所有25个操作再发一遍,否则可能导致重复写入。DynamoDB的Put操作是幂等的,但如果你的业务逻辑在写入前做过条件检查,重复Put可能不会有问题,Delete操作重复执行也是安全的,不过为了清晰和效率,只重试未处理项是最佳实践。第二,指数退避的初始延迟不要设置得太长,100毫秒即可,因为很多限流在几百毫秒内就会恢复,一开始等5秒反而浪费时间。
另一个容易忽视的点是跨表写入时的重试粒度。一个BatchWriteItem请求可以同时操作多个表,比如两个表各放12个和13个操作。如果返回的UnprocessedItems只包含表A的操作,那么重试时只需要提交表A的RequestItems,不能把表B的操作也混进去。上面的代码通过直接传递response.UnprocessedItems保持了这种隔离,这正是推荐做法。
性能优化与设计层面的绕行方案
既然单次25条和16MB的限制是硬性的,如何在高吞吐写入场景下保证效率?第一个思路是客户端并发拆分。不要顺序发送多个BatchWriteItem,而是使用一个线程池或异步任务数组,把大集合按25条一组拆开后并发发出。例如需要写入1000条记录,就拆成40个BatchWriteItem请求,每批25条,然后同时发出其中10到20个(取决于客户端网络和CPU能力)。这样总吞吐可以接近单请求限制的倍数,而不会因为等待顺序响应而拖慢速度。
第二个思路是避免超大数据条目进入批量写入。如果某些记录本身就接近400KB,比如日志分析结果或者图片元数据,那么25个这样的记录很容易突破16MB总大小限制。此时不应该把它们硬塞进BatchWriteItem,而是考虑使用DynamoDB的PutItem单个写入,或者将这些大对象拆分成多个条目存储,比如把大JSON拆成多个行,用同一个分区键加不同的排序键。
第三个思路是结合Kinesis Data Streams或SQS做异步缓冲。对于瞬时峰值极高但平均吞吐可控的场景,直接调用BatchWriteItem可能会因为预置容量不足而产生大量UnprocessedItems。可以先把写入请求发送到SQS队列,然后由后台消费者以稳定的速率分批调用BatchWriteItem,并配合上面的重试逻辑。这样既平滑了流量尖峰,又避免了因为限流而丢弃数据。
最后要强调的是,BatchWriteItem并不提供事务性。如果你需要多个操作要么全部成功要么全部失败,请使用TransactWriteItems。TransactWriteItems同样有25个操作的限制,但额外提供了原子性和条件检查能力,代价是消耗双倍的写容量单位。对于大多数非事务场景,BatchWriteItem配合正确的重试机制已经足够可靠。
DynamoDBBatchWriteItem批量写入限制修改时间:2026-09-25 00:07:27