DynamoDB 是 AWS 提供的全托管 NoSQL 数据库,具备单毫秒级延迟和自动扩容能力,适合高并发读写场景。PHP 项目接入 DynamoDB 时,最省力的方式是使用官方 aws-sdk-php 库中的 DynamoDbClient。客户端会把签名、重试、端点发现和响应解析全部处理好,开发者只需要关注表结构、主键和条件表达式。不过 DynamoDB 的数据模型与关系型数据库差异较大,尤其是属性值的类型映射和查询限制,初次接触很容易写出效率低下的 Scan 操作。这篇文章从实际项目角度出发,介绍如何在 PHP 中完成 DynamoDB 的常用操作。

安装与初始化 DynamoDB 客户端
安装 aws-sdk-php 推荐使用 Composer。在项目根目录执行以下命令即可引入完整 SDK,也可以只选择 DynamoDB 相关组件,但完整安装更方便调试其他 AWS 服务。安装完成后,需要配置访问凭证,常见的方式有环境变量、AWS 配置文件以及 IAM 角色。生产环境建议使用 IAM 角色,避免在代码中硬编码密钥。
客户端初始化时必须指定区域和版本,region 参数对应 DynamoDB 表所在的 AWS 区域,version 参数建议固定为 latest,让 SDK 自动选择最新的 API 版本。如果使用凭据链,可以不显式传入 key 和 secret,SDK 会按照环境变量、配置文件、元数据服务的顺序自动查找凭证。
require 'vendor/autoload.php';
use AwsDynamoDbDynamoDbClient;
use AwsDynamoDbMarshaler;
$client = new DynamoDbClient([
'region' => 'ap-southeast-1',
'version' => 'latest',
// 不传 credentials 时自动使用默认凭证链
]);
$marshaler = new Marshaler();
上面的代码创建了 DynamoDbClient 实例和一个 Marshaler 对象。Marshaler 用于在 PHP 数组和 DynamoDB 属性值格式之间进行转换,后面所有读写操作都会用到它。没有 Marshaler 时,必须手动构造类似 ['S' => 'value'] 的结构,代码会变得冗长且容易出错。区域选择要跟后续建表和读写操作保持一致,否则会报 ResourceNotFoundException。
客户端初始化完成后,可以通过 listTables 方法快速验证配置是否生效。如果返回了表名数组,说明凭证和网络连接都正常。接下来进入数据模型设计和表创建阶段。
数据模型设计与表操作
DynamoDB 表没有固定的列定义,每行数据可以有不同的属性,但必须指定主键。主键分为两种:仅分区键和分区键加排序键。分区键决定数据在物理存储中的分布,排序键则允许在同一个分区内按顺序存储和范围查询。设计主键时需要充分考虑访问模式,因为 DynamoDB 的查询能力高度依赖主键结构。
创建表时通过 AttributeDefinitions 声明主键属性的名称和类型,再通过 KeySchema 指定哪些属性作为分区键和排序键。下面演示创建一个用户表,分区键为 UserId,排序键为 CreatedAt,类型分别为字符串和数字。BillingMode 使用 PAY_PER_REQUEST 可以避免手动配置容量单位,适合流量不可预测的应用。
$result = $client->createTable([
'TableName' => 'Users',
'AttributeDefinitions' => [
['AttributeName' => 'UserId', 'AttributeType' => 'S'],
['AttributeName' => 'CreatedAt', 'AttributeType' => 'N'],
],
'KeySchema' => [
['AttributeName' => 'UserId', 'KeyType' => 'HASH'],
['AttributeName' => 'CreatedAt', 'KeyType' => 'RANGE'],
],
'BillingMode' => 'PAY_PER_REQUEST',
]);
创建表是异步操作,表状态会从 CREATING 变为 ACTIVE。可以使用 waitUntil 方法阻塞等待,避免后续操作时表尚未就绪。列出所有表名可以用 listTables,查看表结构和状态用 describeTable。删除表则使用 deleteTable,同样需要等待删除完成。生产环境删除表前务必确认数据已经备份。
除了基础键属性,DynamoDB 还支持二级索引。全局二级索引可以创建全新的分区键和排序键,适合多种查询模式。本地二级索引只能使用与主表相同的分区键,但允许不同的排序键。二级索引会在后面的查询部分详细说明。
执行写入、读取与更新操作
写入数据使用 putItem 方法,它会根据主键完成新增或覆盖。如果希望仅当主键不存在时才写入,需要添加 ConditionExpression。借助 Marshaler,可以直接将 PHP 关联数组转换为 DynamoDB 属性值格式,不需要手动拼装 'S'、'N' 等类型标识。下面写入一条用户记录,包含 UserId、Name、CreatedAt 和 Status 属性。
$item = [
'UserId' => 'user_1001',
'CreatedAt' => 1715000000,
'Name' => 'Alice',
'Status' => 'ACTIVE',
];
$client->putItem([
'TableName' => 'Users',
'Item' => $marshaler->marshalItem($item),
]);
读取单条记录用 getItem,必须提供完整主键,即分区键和排序键都要给出。返回结果中的 Item 字段是 DynamoDB 格式,使用 unmarshalItem 可以还原为 PHP 数组。如果需要强一致性读取,可以设置 ConsistentRead 为 true,但会消耗更多读取容量。默认的最终一致性读取延迟更低。
更新数据时建议使用 updateItem,通过 UpdateExpression 只修改指定属性,避免覆盖未变更的字段。条件表达式可以防止并发更新冲突。下面的例子将用户状态改为 INACTIVE,并增加一个登录次数字段,使用 SET 和 ADD 操作符完成原子更新。
$client->updateItem([
'TableName' => 'Users',
'Key' => $marshaler->marshalItem([
'UserId' => 'user_1001',
'CreatedAt' => 1715000000,
]),
'UpdateExpression' => 'SET #status = :newStatus ADD LoginCount :delta',
'ExpressionAttributeNames' => [
'#status' => 'Status',
],
'ExpressionAttributeValues' => $marshaler->marshalItem([
':newStatus' => 'INACTIVE',
':delta' => 1,
]),
]);
删除数据用 deleteItem,也可以配合条件表达式实现乐观锁删除。批量操作方面,batchWriteItem 一次最多可以写入或删除 25 个项目,适合初始化数据或清理数据任务。
查询与索引的使用
DynamoDB 的查询分为 query 和 scan。query 基于主键或二级索引进行精确查找和范围查找,效率高且消耗容量可预测;scan 则扫描整张表,会消耗大量读取容量,生产环境应尽量避免使用 scan。query 需要指定 KeyConditionExpression,该表达式只能针对分区键和排序键。
如果主键设计无法满足某些查询需求,可以创建全局二级索引。例如 Users 表以 UserId 为分区键,如果需要按邮箱查找用户,可以创建以 Email 为分区键的 GSI。查询 GSI 时需要指定 IndexName,并传入索引对应的键条件。创建带 GSI 的表时,需要在 AttributeDefinitions 中加入索引键,并在 GlobalSecondaryIndexes 中声明索引结构。
$result = $client->query([
'TableName' => 'Users',
'IndexName' => 'EmailIndex',
'KeyConditionExpression' => 'Email = :email',
'ExpressionAttributeValues' => $marshaler->marshalItem([
':email' => 'alice@ippipp.com',
]),
]);
foreach ($result['Items'] as $item) {
$user = $marshaler->unmarshalItem($item);
echo $user['UserId'] . PHP_EOL;
}
查询结果默认最多返回 1MB 数据,如果结果集更大,会包含 LastEvaluatedKey 字段。此时需要循环请求,将 LastEvaluatedKey 作为 ExclusiveStartKey 传入下一次 query。SDK 还提供了 Paginator 封装,可以自动完成分页遍历,减少样板代码。
$paginator = $client->getPaginator('Query', [
'TableName' => 'Users',
'IndexName' => 'EmailIndex',
'KeyConditionExpression' => 'Email = :email',
'ExpressionAttributeValues' => $marshaler->marshalItem([
':email' => 'alice@ippipp.com',
]),
]);
foreach ($paginator as $page) {
foreach ($page['Items'] as $item) {
$user = $marshaler->unmarshalItem($item);
echo $user['UserId'] . PHP_EOL;
}
}
条件表达式与错误处理
条件表达式在写入和更新时非常有用,它基于已有属性值判断操作是否应该执行。典型的场景包括:仅当记录不存在时插入、仅当版本号匹配时更新、仅当状态字段满足条件时修改。ConditionExpression 语法与 UpdateExpression 和 KeyConditionExpression 类似,使用占位符和 ExpressionAttributeValues 注入具体值。
下面的代码演示了使用条件写入实现幂等创建:只有 UserId 不存在时才写入,如果主键已经存在会抛出 ConditionalCheckFailedException。这样避免了覆盖已有用户数据的风险。
try {
$client->putItem([
'TableName' => 'Users',
'Item' => $marshaler->marshalItem($item),
'ConditionExpression' => 'attribute_not_exists(UserId)',
]);
} catch (AwsDynamoDbExceptionDynamoDbException $e) {
if ($e->getAwsErrorCode() === 'ConditionalCheckFailedException') {
echo "记录已存在,跳过写入" . PHP_EOL;
} else {
throw $e;
}
}
DynamoDB 的异常通常继承自 DynamoDbException,除了条件检查失败,还有吞吐量超限、资源未找到和请求冲突等类型。生产环境应该根据错误码实施不同的重试策略。默认情况下 SDK 已经内置了指数退避重试,对于 ProvisionedThroughputExceededException 会最多重试三次。如果使用按需容量,这类错误基本不会出现。
为了提升写入和读取性能,可以组合使用 batchGetItem 和 batchWriteItem。这两个方法支持一次性操作多个项目,但单次请求有大小和数量限制,需要自行处理未处理的项目。对于大型数据导出或迁移场景,优先使用 AWS Data Pipeline 或 DMS,而不是通过 PHP 逐条操作。
连接到 DynamoDB 时建议启用 HTTP 连接复用,并在客户端初始化时合理设置超时参数。对于短生命周期的 PHP 进程,如 Lambda 函数,可以利用执行环境重用客户端实例,减少初始化开销。对于长时间运行的后台进程,定期检查凭证是否过期,并捕获 CredentialsProvider 抛出的异常。
数据类型映射与序列化细节
DynamoDB 的每条属性值都包含类型描述,例如字符串表示为 ['S' => 'hello'],数字表示为 ['N' => '123'],二进制数据表示为 ['B' => 'base64string']。Marshaler 会自动推断 PHP 变量类型,整数和浮点数会转换为 N 类型,字符串转换为 S 类型,布尔值转换为 BOOL 类型,数组则根据是否为关联数组转换为 M 或 L 类型。
空值在 DynamoDB 中不能直接存储,如果 PHP 数组包含 null 值,marshalItem 会抛出异常。处理可选字段时,应该在写入前过滤掉 null 值,或者将 null 转换为默认字符串或数字。对于空数组和空对象,也需要留意 Marshaler 的行为,避免生成空 Map 或 List 导致后续查询异常。
读取数据时,unmarshalItem 会将 DynamoDB 类型还原为 PHP 原生类型。数字类型在 DynamoDB 中以字符串传输,但 unmarshal 后会自动转换为 int 或 float。如果数字精度要求较高,比如时间戳或金额,建议 PHP 端使用字符串处理,避免浮点精度丢失。创建表时属性类型必须与写入数据类型一致,否则会报 ValidationException。
掌握 Marshaler 的类型映射规则后,可以在 PHP 模型层封装一层 Repository,将领域对象与 DynamoDB 属性格式彻底隔离。这样业务代码不感知底层存储细节,未来迁移到其他 NoSQL 数据库时改动范围更小。
监控与性能调优建议
DynamoDB 提供了 CloudWatch 指标,包括读取容量消耗、写入容量消耗、节流事件和系统延迟。PHP 应用在调试阶段可以重点观察 ConsumedReadCapacityUnits 和 ConsumedWriteCapacityUnits 指标,当频繁接近表级别限制时,需要考虑调整主键设计或改用按需容量模式。
查询性能优化主要依靠合理的主键和二级索引设计。避免使用 scan,除非表数据量极小且访问频率很低。对于时间序列数据,可以使用分区键加时间戳排序键的方式,把最近数据放在同一个分区内,并定期归档旧数据到 S3 或冷存储。写入热点问题可以通过在分区键上增加随机后缀来解决。
连接层面,PHP 的 DynamoDbClient 使用 Guzzle 作为 HTTP 客户端,默认会启用 Keep-Alive。在长驻进程中,建议复用客户端实例,而不是每次请求都重新创建。对于 Lambda 环境,可以利用全局静态变量缓存客户端和 Marshaler,减少冷启动时间。同时注意 IAM 角色的权限最小化,只授予必要的 dynamodb 操作权限。
错误重试策略可以在客户端初始化时通过 retries 参数调整。对于幂等写入操作可以增加重试次数,而对于非幂等操作,需要通过条件表达式保证重复执行不会产生副作用。监控重试次数和错误率,有助于发现容量不足或权限配置错误等问题。
DynamoDBaws-sdk-phpPHP修改时间:2026-08-19 20:45:48