You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js v3 SDK下AWS Lambda DynamoDB分页最佳实现方案

方案选型结论

分页是该场景下的最优实时查询方案,没有之一。
不要尝试通过调大Lambda超时阈值(Lambda最大超时也只有15分钟,表数据持续增长迟早会撞线)、提升Lambda内存配置的方式解决全量Scan超时问题:DynamoDB的Scan操作单次请求最多返回1MB数据,原生就是分页设计,单次请求硬拉全量本质是反模式,只要表数据量持续上涨,超时问题必然复现。
如果你的业务场景不是要求实时返回最新全量数据,优先选择DynamoDB全量导出到S3、提前生成静态快照供调用方下载的方案,成本仅为实时Scan的1/10不到,性能也更稳定;只有需要实时查询最新数据的场景,才用实时分页Scan方案。
注意分页实现必须做无状态设计:不要在Lambda内存、Redis等存储里维护分页游标,直接透传DynamoDB原生返回的LastEvaluatedKey即可,实现简单还不会有状态不一致问题。

Node.js v3 SDK 最佳实现

首先明确避坑点:不要使用SDK自带的paginateScan方法处理面向前端的单页请求,这个方法是封装来给服务端批量拉取全量数据用的,会自动循环发起请求拉取所有分页,大表场景下照样会触发Lambda超时。

前置准备

  • 给Lambda执行角色配置对应DynamoDB表的dynamodb:Scan权限
  • 安装依赖:
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

推荐使用@aws-sdk/lib-dynamodb提供的DocumentClient,无需手动处理DynamoDB的AttributeValue类型转换,直接读写原生JS对象即可。

完整实现代码

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";

// 客户端实例初始化放在Handler外部,跨Lambda调用复用TCP连接,降低冷启动开销
const ddbClient = new DynamoDBClient({ region: "替换为你的AWS区域" });
const docClient = DynamoDBDocumentClient.from(ddbClient);

// 配置项
const TABLE_NAME = "替换为你的DynamoDB表名";
const MAX_PAGE_SIZE = 100; // 单页最大条数,根据单条数据大小调整,建议不超过100
const DEFAULT_PAGE_SIZE = 20; // 默认单页条数

export const handler = async (event) => {
  try {
    // 适配API Gateway集成的GET请求参数,从queryString取分页参数
    const queryParams = event.queryStringParameters || {};
    const { pageSize, nextToken } = queryParams;

    // 分页参数校验
    const validPageSize = Math.min(
      Number(pageSize) || DEFAULT_PAGE_SIZE,
      MAX_PAGE_SIZE
    );
    if (validPageSize <= 0) {
      return {
        statusCode: 400,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ msg: "pageSize必须为正整数" })
      };
    }

    // 构造Scan请求参数
    const scanConfig = {
      TableName: TABLE_NAME,
      Limit: validPageSize
    };

    // 传入了分页游标时,解码后作为查询起点
    if (nextToken) {
      try {
        scanConfig.ExclusiveStartKey = JSON.parse(
          Buffer.from(nextToken, "base64").toString("utf8")
        );
      } catch (err) {
        return {
          statusCode: 400,
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ msg: "nextToken格式非法" })
        };
      }
    }

    // 发起单次Scan请求,仅获取1页数据
    const scanRes = await docClient.send(new ScanCommand(scanConfig));

    // 构造返回结果:存在下一页时,将LastEvaluatedKey编码为base64字符串作为nextToken返回
    return {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*" // 按需配置跨域
      },
      body: JSON.stringify({
        list: scanRes.Items,
        pageSize: validPageSize,
        nextToken: scanRes.LastEvaluatedKey
          ? Buffer.from(JSON.stringify(scanRes.LastEvaluatedKey)).toString("base64")
          : null
      })
    };
  } catch (err) {
    console.error("DynamoDB Scan执行失败:", err);
    return {
      statusCode: 500,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ msg: "服务内部错误" })
    };
  }
};

调用逻辑说明

  • 首次请求无需传nextToken,接口返回第一页数据
  • 当返回的nextToken不为null时,代表还有下一页数据,下次请求把该值放到query参数里传给接口即可拉取后续页
  • 当返回的nextToken为null时,代表已经拉取完全部数据
额外优化建议
  • 单页大小不要盲目设大:DynamoDB单次Scan请求无论设置多大Limit,最多只会返回1MB数据,如果单条数据体积较大,实际返回条数会小于传入的Limit,属于正常现象,调用方只要判断nextToken不为空就继续拉取即可
  • 尽量避免无过滤条件的全表Scan:如果业务有按字段过滤、排序的需求,优先给对应字段创建GSI,用Query操作替代Scan,读容量消耗仅为Scan的几十分之一,延迟也更稳定
  • 高并发场景下记得开启DynamoDB读容量自动伸缩,避免突发大流量Scan触发读限流
  • 如果场景是定期全量数据同步,不要用实时分页接口拉取,优先用DynamoDB原生导出到S3功能、配合DynamoDB Streams做增量同步,成本和稳定性远优于实时Scan

内容的提问来源于stack exchange,提问作者user15951701

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:15:44