DynamoDBではトランザクションが使用できません。
…でしたが最近使用できるようになったみたいです。(2018-11-28)
https://aws.amazon.com/jp/blogs/news/new-amazon-dynamodb-transactions/
というわけで早速試してみました!
実行環境
- php7.2
- AWS SDK for PHP - Version 3
- AWS-東京リージョン(ap-northeast-1)
##準備
1. aws/aws-sdk-php
composer.jsonに以下のように記述します。
{
"require": {
"aws/aws-sdk-php":"*"
}
}
composerコマンドでインストールを行います。
composer install
2. DynamoDbClient
DynamoDBにアクセスするためのクライアントを返すphpを作成しておきます。
<?php
use Aws\DynamoDb\DynamoDbClient;
$dynamodb = DynamoDbClient::factory([
'credentials' => [
'key' => 'AWSのアクセスキー',
'secret' => 'AWSのシークレットキー',
],
//東京リージョン
'region' => 'ap-northeast-1',
'version' => 'latest'
]);
return $dynamodb;
3. テーブル作成
DynamoDBのテーブルを作成するphpを作成します。
今回はトランザクションを試したいので2テーブル作成します。
(ちなみにこの作業はコンソールからでも大丈夫です。)
<?php
use Aws\DynamoDb\Exception\DynamoDbException;
//DynamoDbClient
$dynamodb = require_once('client.php');
//作成するテーブルのスキーマ
$params = [
[
'TableName' => 'users',
'KeySchema' => [
[
'AttributeName' => 'name',
'KeyType' => 'HASH'
],
],
'AttributeDefinitions' => [
[
'AttributeName' => 'name',
'AttributeType' => 'S'
],
],
'ProvisionedThroughput' => [
'ReadCapacityUnits' => 10,
'WriteCapacityUnits' => 10
]
],
[
'TableName' => 'logs',
'KeySchema' => [
[
'AttributeName' => 'name',
'KeyType' => 'HASH'
],
[
'AttributeName' => 'created_at',
'KeyType' => 'RANGE'
],
],
'AttributeDefinitions' => [
[
'AttributeName' => 'name',
'AttributeType' => 'S'
],
[
'AttributeName' => 'created_at',
'AttributeType' => 'S'
],
],
'ProvisionedThroughput' => [
'ReadCapacityUnits' => 10,
'WriteCapacityUnits' => 10
]
]
];
try {
foreach ($params as $param) {
//テーブル作成
$response = $dynamodb->createTable($param);
if ($response['@metadata']['statusCode'] !== 200) {
echo 'Failed.';
exit();
}
}
//すでにテーブルが作成されている場合、例外処理が発生する
} catch (DynamoDbException $e) {
var_dump($e->getMessage());
exit();
}
echo 'Successfully.';
上記のcreateTable.phpを実行すると、usersとlogsという名前の2テーブルが作成されます。
##実行
TransactWriteItems
トランザクションでデータを書き込むTransactWriteItemsを試してみます。
usersテーブルとlogsテーブルにデータを追加するphpを作成します。
- シナリオ
- 所持金300円のAさんが所持金0円のBさんに300円振り込む。
- 更にAさんがBさんに300円振り込もうとする。
- Aさんの所持金が-300円になってしまうため、2番の処理をロールバックする。
###1.初期データ準備
まず、初期データ(名前、 所持金)を流し込むphpを作成します。
<?php
use Aws\DynamoDb\Exception\DynamoDbException;
use Aws\DynamoDb\Marshaler;
//DynamoDbClient
$dynamodb = require_once('client.php');
$marshaler = new Marshaler();
$dynamodb->putItem([
'TableName' => 'users',
'Item' => [
'name' => $marshaler->marshalValue('A'),
'balance' => $marshaler->marshalValue(300),
],
]);
$dynamodb->putItem([
'TableName' => 'users',
'Item' => [
'name' => $marshaler->marshalValue('B'),
'balance' => $marshaler->marshalValue(0),
],
]);
上記のphpを実行すると下記のようにデータが作成されます。
###2.トランザション処理(commit)
振込処理のphpを作成します。
<?php
use Aws\DynamoDb\Exception\DynamoDbException;
use Aws\DynamoDb\Marshaler;
//DynamoDbClient
$dynamodb = require_once('client.php');
$marshaler = new Marshaler();
try {
$response = $dynamodb->transactWriteItems([
'TransactItems' => [
//Bの残高を300足す
[
'Update' => [
'TableName' => 'users',
'Key' => $marshaler->marshalItem([
'name' => 'B'
]),
'ExpressionAttributeValues' => [
':val' => $marshaler->marshalValue(300),
],
'UpdateExpression' => 'SET balance = balance + :val',
]
],
//+300をログに記録
[
'Put' => [
'TableName' => 'logs',
'Item' => $marshaler->marshalItem([
'name' => 'B',
'process' => '+300',
'created_at' => date('Y-m-d H:i:s'),
]),
]
],
//Aの残高から300引く
[
'Update' => [
'TableName' => 'users',
'Key' => $marshaler->marshalItem([
'name' => 'A'
]),
'ExpressionAttributeValues' => [
':val' => $marshaler->marshalValue(300),
],
'UpdateExpression' => 'SET balance = balance - :val',
'ConditionExpression' => ':val <= balance'
]
],
//-300円をログに記録
[
'Put' => [
'TableName' => 'logs',
'Item' => $marshaler->marshalItem([
'name' => 'A',
'process' => '-300',
'created_at' => date('Y-m-d H:i:s'),
]),
]
],
]
]);
if ($response['@metadata']['statusCode'] !== 200) {
echo 'Failed.';
exit();
}
} catch (DynamoDbException $e) {
var_dump($e->getAwsErrorCode());
exit();
}
echo 'Successfully.';
上記を実行すると、以下のような結果になります。
###3.トランザション処理(rollback)
この状態でtransact.phpをもう一度実行します。
そうすると、Aの残高を-300円する処理でTransactionCanceledExceptionが発生します。
(ConditionExpressionで振込額が残高を超えないよう条件を指定しているため)
結果的に処理が実行されず、データは変更されません。
ちなみにConditionExpressionを外して実行すると以下の結果になります。
##所感
今まで、DynamoDBはトランザクションに未対応だったのでRDSから移行するのにはボトルネックとなっていました。(Amazon SQSなどを併用して代替的に実現する方法はありましたが)
公式ドキュメントを見ると、テーブルロックはされないみたいですが、トランザクション分離レベルはserializableとなっており安全にデータを操作できそうです。
ただ、トランザクション内の各Itemに対して2回の読み取りまたは書き込みを行うようなので、使用は最小限に留めておいたほうが良さそうですね。
今回のトランザクション対応でDynamoDBを起用できるケースも増えるのではないしょうか。