Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

talow1
主にPHPやってます。
fork
株式会社フォークは、Webサイトの企画・制作・開発・サーバホスティング・コンタクトセンターを一社に集約したワンストップソリューションを展開する制作会社です。
https://www.fork.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away