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を起用できるケースも増えるのではないしょうか。







 FORK Advent Calendar 2018
 FORK Advent Calendar 2018