11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FORKAdvent Calendar 2018

Day 23

PHPでDynamoDBのトランザクションを試してみた

Last updated at Posted at 2018-12-22

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に以下のように記述します。

composer.json
{
    "require": {
    	"aws/aws-sdk-php":"*"
    }
}

composerコマンドでインストールを行います。

composer install

2. DynamoDbClient

DynamoDBにアクセスするためのクライアントを返すphpを作成しておきます。

client.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テーブル作成します。
(ちなみにこの作業はコンソールからでも大丈夫です。)

createTable.php
<?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を作成します。

  • シナリオ
  1. 所持金300円のAさんが所持金0円のBさんに300円振り込む。
  2. 更にAさんがBさんに300円振り込もうとする。
  3. Aさんの所持金が-300円になってしまうため、2番の処理をロールバックする。

###1.初期データ準備
まず、初期データ(名前、 所持金)を流し込むphpを作成します。

initial.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を実行すると下記のようにデータが作成されます。

  • usersテーブル
    users.PNG

###2.トランザション処理(commit)
振込処理のphpを作成します。

transact.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.';

上記を実行すると、以下のような結果になります。

  • usersテーブル
    users.PNG

  • logsテーブル
    logs.PNG

###3.トランザション処理(rollback)
この状態でtransact.phpをもう一度実行します。
そうすると、Aの残高を-300円する処理でTransactionCanceledExceptionが発生します。
(ConditionExpressionで振込額が残高を超えないよう条件を指定しているため)

結果的に処理が実行されず、データは変更されません。

ちなみにConditionExpressionを外して実行すると以下の結果になります。

  • usersテーブル
    users.PNG

  • logsテーブル
    logs.PNG

##所感
今まで、DynamoDBはトランザクションに未対応だったのでRDSから移行するのにはボトルネックとなっていました。(Amazon SQSなどを併用して代替的に実現する方法はありましたが)

公式ドキュメントを見ると、テーブルロックはされないみたいですが、トランザクション分離レベルはserializableとなっており安全にデータを操作できそうです。

ただ、トランザクション内の各Itemに対して2回の読み取りまたは書き込みを行うようなので、使用は最小限に留めておいたほうが良さそうですね。

今回のトランザクション対応でDynamoDBを起用できるケースも増えるのではないしょうか。


:christmas_tree: FORK Advent Calendar 2018

11
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?