Spring Cloud AWS 把 AWS 的多种服务封装成 Spring Boot 风格的自动配置模块,其中 DynamoDB 集成组件允许开发者通过 DynamoDbTemplate 或 DynamoDbOperations 直接操作表数据,配合 @DynamoDbBean、@DynamoDbPartitionKey 等注解完成对象映射。但要把这套组合在生产环境用好,不能只停留在增删改查层面,还需要理解主键设计、GSI 索引、条件写入以及容量模式对性能和费用的影响。下面会从连接配置、实体映射、查询优化和批量操作几个角度展开。

快速搭建 Spring Cloud AWS 与 DynamoDB 的连接
首先需要在 Spring Boot 工程中引入 spring-cloud-aws-starter-dynamodb 依赖。如果使用 Maven,可以在 pom.xml 中加入以下配置,注意版本要和 Spring Cloud AWS 的 BOM 对齐,避免出现客户端类找不到的问题。
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-dynamodb</artifactId>
</dependency>
接下来在 application.yml 中配置区域、访问凭证以及 DynamoDB 端点。对于本地开发,通常会指向 http://localhost:8000 的 DynamoDB Local,生产环境则直接使用默认的 AWS 端点。凭证可以通过环境变量、IAM 角色或显式的 access-key 提供,但显式写入配置文件有泄露风险,建议使用默认凭证链。
spring:
cloud:
aws:
credentials:
access-key: ${AWS_ACCESS_KEY_ID}
secret-key: ${AWS_SECRET_ACCESS_KEY}
region:
static: us-east-1
dynamodb:
endpoint: http://localhost:8000
配置完成后,Spring 容器会自动创建 DynamoDbClient 和 DynamoDbTemplate Bean,可以直接注入使用。需要说明的是,如果只做本地测试,endpoint 配成 DynamoDB Local 地址,region 可以随意指定,但表创建和读写都会落到本地实例。
连接层稳定之后,还需要关注客户端超时和重试策略。默认的 AWS SDK 客户端有自动重试机制,但在某些高延迟网络环境下,过度的重试可能放大请求压力。可以通过自定义 DynamoDbClient 的 overrideConfiguration 调整重试次数和超时时间,让读写行为更符合业务预期。
实体建模与主键设计:避免热分区
DynamoDB 的表结构以主键为核心,主键分为分区键和可选的排序键。分区键决定数据存储在哪个物理分区,设计不当会造成热分区,比如把所有用户数据都放在同一个分区键下,写入和读取都会集中在同一个分区,吞吐量受限。一个常见的优化方式是使用高基数的分区键,例如用户 ID、订单 ID 等,而不是状态、类型这类低基数字段。
使用 Spring Cloud AWS 的注解可以很方便地把 Java 对象映射到 DynamoDB 表。下面定义一个 Product 实体,使用 @DynamoDbBean 标记类,用 @DynamoDbPartitionKey 标注分区键字段,排序键则用 @DynamoDbSortKey。如果表只有分区键,可以省略排序键。
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey;
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey;
@DynamoDbBean
public class Product {
private String category;
private String productId;
private String name;
private double price;
@DynamoDbPartitionKey
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
@DynamoDbSortKey
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
这个例子中 category 作为分区键,productId 作为排序键。如果某个分类下的商品数量极大,仍然可能产生热分区,因此实际项目中需要结合访问模式选择分区键。比如将 category 和 productId 的哈希值拼接作为分区键,或者使用租户 ID 加业务 ID 的组合来分散负载。
除了注解映射,还可以使用静态的 TableSchema 构建器。注解方式更直观,适合大多数 CRUD 场景;静态 schema 方式可以避免反射带来的性能开销,并且能在编译期检查字段映射错误。对于读多写少的表,建议优先使用注解方式减少样板代码。
使用 DynamoDbTemplate 完成读写与条件更新
Spring Cloud AWS 提供的 DynamoDbTemplate 封装了常见的增删改查操作,调用方式和 JdbcTemplate 类似。插入一条记录可以用 save 方法,读取单条记录需要传入分区键和可选的排序键。下面展示如何保存和查询一个 Product 对象。
@Autowired
private DynamoDbTemplate dynamoDbTemplate;
public void saveProduct(Product product) {
dynamoDbTemplate.save(product);
}
public Product getProduct(String category, String productId) {
Key key = Key.builder()
.partitionValue(category)
.sortValue(productId)
.build();
return dynamoDbTemplate.load(key, Product.class);
}
这里 Key.builder() 来自 AWS SDK 的 enhanced client,partitionValue 和 sortValue 需要与实体定义的主键顺序一致。如果只按分区键读取,会返回一组记录,可以用 query 方法配合条件表达式实现。
更新操作不能简单调用 save 覆盖,因为 DynamoDB 的 save 本质是 putItem,会整条替换。如果只想修改某个字段,应该使用 update 方法并指定更新表达式。Spring Cloud AWS 的 DynamoDbTemplate 支持 UpdateItemEnhancedRequest,可以精确控制要更新的属性和条件。
public void updatePrice(String category, String productId, double newPrice) {
Key key = Key.builder()
.partitionValue(category)
.sortValue(productId)
.build();
Map<String, AttributeValue> expressionValues = Map.of(
":newPrice", AttributeValue.builder().n(String.valueOf(newPrice)).build()
);
UpdateItemEnhancedRequest<Product> request = UpdateItemEnhancedRequest.builder(Product.class)
.item(key)
.updateExpression("SET price = :newPrice")
.expressionValues(expressionValues)
.build();
dynamoDbTemplate.update(request);
}
条件更新是避免并发覆盖的重要手段。可以在请求中加入 conditionExpression,例如只有当 price 小于某个值时才更新,否则抛出 ConditionalCheckFailedException。这种乐观锁机制非常适合库存扣减、订单状态流转等场景,不需要额外的分布式锁。
读取数据时还要注意强一致性与最终一致性的选择。DynamoDB 默认读取是最终一致的,成本更低,但可能读到旧数据。如果业务要求读后写一致,可以在 query 或 getItem 请求中开启强一致性读取。Spring Cloud AWS 的模板方法通常允许传入 ReadMode 或者通过 DynamoDbClient 原生 API 控制。
GSI 索引与查询模式优化
DynamoDB 的主键只能支撑一种访问模式,当业务需要按其他字段查询时,就要创建全局二级索引(GSI)。GSI 有自己的分区键和排序键,可以与主表不同,并且可以独立设置容量。Spring Cloud AWS 的实体映射支持 @DynamoDbSecondaryPartitionKey 和 @DynamoDbSecondarySortKey 注解,用来标注 GSI 的键字段。
例如 Product 表主键是 category 加 productId,现在需要按 name 查询商品,可以创建一个名为 name-index 的 GSI,将 name 作为分区键。在实体类中标注对应字段,然后创建表时通过 DynamoDbEnhancedClient 的 table 方法指定 GSI 定义。
@DynamoDbBean
public class Product {
private String category;
private String productId;
private String name;
private double price;
@DynamoDbPartitionKey
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
@DynamoDbSortKey
public String getProductId() { return productId; }
public void setProductId(String productId) { this.productId = productId; }
@DynamoDbSecondaryPartitionKey(indexNames = "name-index")
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
上面的代码将 name 字段映射为 name-index 这个 GSI 的分区键。查询时必须使用 query 方法并指定索引名,不能直接用主键的 load 方法。Spring Cloud AWS 的 DynamoDbTemplate 支持传入 IndexName 构造查询请求。
GSI 虽然灵活,但会增加写入成本和存储成本,因为每次写主表都会同步更新所有 GSI。创建 GSI 前应该梳理业务查询需求,只创建必要的索引。同时 GSI 的容量需要单独配置,如果使用按需容量模式,成本直接与读写流量挂钩,所以不要为了偶尔一次的查询创建长期存在的 GSI。
另一个常见误区是使用 Scan 操作遍历整张表。Scan 会读取所有分区,消耗大量读容量,且响应时间随数据量线性增长。除了后台对账、全量导出等低频场景,业务查询应尽量使用 Query 配合主键或 GSI。如果需要模糊搜索,建议把数据同步到 OpenSearch 等搜索引擎,而不是在 DynamoDB 上做 Scan。
事务、批量写入与容量模式选择
涉及多个表或多个条目的一致性操作时,可以使用 DynamoDB 的事务写入。Spring Cloud AWS 的 DynamoDbTemplate 支持 transactWriteItems 方法,但更简单的做法是直接使用 AWS SDK 的 DynamoDbClient 构建 TransactWriteItemsRequest。事务操作会消耗双倍写容量,并且有 100 条操作的限制,适合订单创建与库存扣减这类强一致场景。
public void createOrderWithStockUpdate(Order order, Stock stock) {
TransactWriteItemsRequest request = TransactWriteItemsRequest.builder()
.transactItems(
TransactWriteItem.builder()
.put(Put.builder()
.tableName("Order")
.item(order.toItem())
.build())
.build(),
TransactWriteItem.builder()
.update(Update.builder()
.tableName("Stock")
.key(stock.toKey())
.updateExpression("SET quantity = quantity - :q")
.expressionAttributeValues(Map.of(":q", AttributeValue.builder().n("1").build()))
.build())
.build()
)
.build();
dynamoDbClient.transactWriteItems(request);
}
对于批量写入非事务场景,可以使用 batchWriteItem,一次请求最多写入 25 个条目,能显著减少 HTTP 请求次数。Spring Cloud AWS 的模板没有直接暴露批量写入方法,但可以通过注入 DynamoDbClient 调用原生 API。批量写入同样需要处理未处理条目,并对超出 16MB 请求大小的部分进行分片重试。
容量模式选择直接关系到成本。按需容量模式适合流量波动大、无法预估峰值的应用,每百万请求计费,但单价较高。预留容量模式适合流量稳定、可以预测的场景,通过购买 WCU 和 RCU 降低单价。Spring Cloud AWS 创建表时可以通过 TableSchema 和 CreateTableRequest 指定 billingMode,默认是按需模式。无论选择哪种模式,都要监控 ThrottlingException 和 ConsumedCapacity,及时调整容量配置。
综上,Spring Cloud AWS 让 DynamoDB 的接入成本大幅降低,但要想真正用好,仍然需要围绕主键、GSI、条件写入和容量模式做出合理设计。把 NoSQL 当 SQL 用是最常见的错误,希望这篇文章能帮助读者避开这些坑,构建出高效且成本可控的数据访问层。

