DynamoDBトランザクションライブラリをちょっぴり試してみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。

AWSにおける代表的なKVS型データベース「DynamoDB」、皆様活用されておりますでしょうか。 RDBはACIDという特性が(略)。一方KVSではBASEという(略)。この辺りについてはAmazon RDSにおけるFallback-Queueingパターンというエントリでご紹介しましたので、参照してください。

さて、そんなDynamoDBでトランザクションを実現するJavaライブラリが出現しました。名前はそのまんま「dynamodb-transactions」。 というわけで早速試してみました。

インストール

Javaプロジェクトでライブラリを利用する際は、Maven等を使ってpom.xml等に依存性を定義するだけで使えると良いのですが、このライブラリは現時点ではどこかのMavenリポジトリにホストされている訳ではないようなので、自分でビルドする必要があります。とは言え、GitとMavenが導入されていれば、以下のコマンドを実行するだけです。

$ git clone https://github.com/awslabs/dynamodb-transactions.git
$ mvn clean source:jar install -Dgpg.skip=true

これで、Mavenのローカルリポジトリにライブラリがインストールされましたので、プロジェクトのpom.xmlに以下のような記述を追加します。

    <dependency>
      <groupId>com.amazonaws.services.dynamodbv2</groupId>
      <artifactId>amazon-dynamodb-transactions</artifactId>
      <version>1.0.0</version>
    </dependency>

また、クラスパスルートにAwsCredentials.propertiesというファイルを作成し、AWSのアクセスキー及びシークレットを記述しておいてください。

#Insert your AWS Credentials from http://aws.amazon.com/security-credentials
accessKey=XXXXXXXXXXXXXXXXXXXX
secretKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

以上で、dynamodb-transactionsを利用する準備が整いました。

テーブルの作成

aws-sdk-javaによって提供されるDynamoDBのAPIラッパーは、v1系となるcom.amazonaws.services.dynamodbと、 v2系のcom.amazonaws.services.dynamodbv2の2種類があります。dynamodb-transactionsはv2系を利用する必要がありますので、インポートミスに注意してください。前半2つがaws-sdk-javaによって提供されるパッケージ、項半2つがdynamodb-transactionsによって提供されるパッケージです。

import com.amazonaws.services.dynamodbv2.*;          // aws-sdk-java
import com.amazonaws.services.dynamodbv2.model.*;      // aws-sdk-java
import com.amazonaws.services.dynamodbv2.transactions.*;  // dynamodb-transactions
import com.amazonaws.services.dynamodbv2.util.*;      // dynamodb-transactions

ではDynamoDBのクライアントインスタンスを作ります。ここまでは普通ですね。

AWSCredentialsProvider cred = new ClasspathPropertiesFileCredentialsProvider();
AmazonDynamoDB dynamodb = new AmazonDynamoDBClient(cred);
dynamodb.setEndpoint("http://dynamodb.ap-northeast-1.amazonaws.com");

あと、このライブラリはあくまでもJavaのプログラムとしてトランザクションを実現するためのライブラリで、DynamoDB自体の機能としてトランザクションが実装された訳ではありません。トランザクションを実現するために、裏側でDynamoDBのテーブルを2つ利用します。それぞれ名前は自由ですが、TransactionsTransactionImagesとすることが多いようです。

その他に、業務データを格納する本来のテーブルを作る必要がありますね。これは今回TransactionExamplesとします。

というわけで、これらのテーブルを作成します。もしテーブルが既に存在したら、新たに作る必要はありません。 無ければ作るのですが、DynamoDBはテーブル作成のリクエストを投げてから利用可能(ACTIVE)になるまでの少々時間が掛かります。 というわけで、作成のリクエストをした後、ACTIVEになるまで待機する必要があります。

というコードを書くのは以外とめんどくさいですね。ということで各種ヘルパーユーティリティが用意されています。便利ぃ。

// 定数定義
static final String TX_TABLE_NAME = "Transactions";
static final String TX_IMAGES_TABLE_NAME = "TransactionImages";
static final String EXAMPLE_TABLE_NAME = "TransactionExamples";
static final String EXAMPLE_TABLE_HASH_KEY = "ItemId";

// Transactionsテーブルの作成
TransactionManager.verifyOrCreateTransactionTable(dynamodb, TX_TABLE_NAME, 1, 1, null);

// TransactionImagesテーブルの作成
TransactionManager.verifyOrCreateTransactionImagesTable(dynamodb, TX_IMAGES_TABLE_NAME, 1, 1, null);

// TransactionExamplesテーブルの作成
List<AttributeDefinition> attributeDefinitions = Arrays.asList(
  new AttributeDefinition().withAttributeName(EXAMPLE_TABLE_HASH_KEY).withAttributeType(ScalarAttributeType.S));
List<KeySchemaElement> keySchema = Arrays.asList(
  new KeySchemaElement().withAttributeName(EXAMPLE_TABLE_HASH_KEY).withKeyType(KeyType.HASH));
ProvisionedThroughput provisionedThroughput = new ProvisionedThroughput()
  .withReadCapacityUnits(1L)
  .withWriteCapacityUnits(1L);
TableHelper tableHelper = new TableHelper(dynamodb);
tableHelper.verifyOrCreateTable(EXAMPLE_TABLE_NAME, attributeDefinitions, keySchema, null,
    provisionedThroughput, null);

ちなみに、TransactionManager.verifyOrCreateTransactionTableの最後のパラメータnullは、 テーブル生成待ちの待機時間(秒)なのですが、nullを指定することによって待たなくなります。 1つが完成した後に2つ目を作り始めるのは時間の無駄ですので、今回は3つのテーブル作成リクエストを投げた後に、それぞれのテーブルがACTIVEになるのを待ちましょう。

tableHelper.waitForTableActive(TX_TABLE_NAME, 5 * 60L);
tableHelper.waitForTableActive(TX_IMAGES_TABLE_NAME, 5 * 60L);
tableHelper.waitForTableActive(EXAMPLE_TABLE_NAME, 5 * 60L);

準備は以上です。

トランザクション機能を確認する

ここからが本題。各種トランザクション機能の使い方を確認していきましょう。 まず、残念ながらJavaはMapを作るのが冗長なので、ちょっとユーティリティメソッドを定義しておきます。

private static Map<String, AttributeValue> createItem(String key) {
  Map<String, AttributeValue> item = new HashMap<String, AttributeValue>();
  item.put(EXAMPLE_TABLE_HASH_KEY, new AttributeValue(key));
  return item;
}

では、2つのputItemリクエストをアトミックに行うコードを書いてみます。

ところで、TransactionManagerっていうクラスを見るとワクワクしませんか? 私だけですか? …そうですか……。

TransactionManager txManager = new TransactionManager(dynamodb, TX_TABLE_NAME, TX_IMAGES_TABLE_NAME);
Transaction t1 = txManager.newTransaction(); // 新規トランザクションの作成
try {
  // AmazonDynamoDBに対してではなく、Transactionオブジェクトに対してputItemを要求していることに注目
  t1.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item1")));
  
  // もう一つのアイテムをputItem要求
  t1.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item2")));
} finally {
  t1.commit(); // コミット!
  t1.delete(); // 完了したトランザクションは削除しておく
}

ではこんな感じのコードを実行して、Management Consoleから、テーブルの状態を見てみましょう。2つのアイテムがありますね。

2013-07-03_1539-td-1

では次に、ロールバックしてみましょうか。

Transaction t2 = txManager.newTransaction();
try {
  t2.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item3")));
  t2.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item4")));
} finally {
  t2.rollback(); // ロールバック!
  t2.delete(); // 完了したトランザクションは削除しておく
}

再びManagement Consoleでテーブルを確認しますが、Item3とItem4は追加されていません。トランザクションの原子性が確認できました。

ちなみに、どちらのケースでも、Transactionをdeleteした後、TransactionsTransactionImagesテーブルには何もアイテムがありません。というわけで、コミット前にThread.sleep(60000);等を挿入して、その間にこれらのテーブルがどうなっているのか覗いてみましょう。

Transaction t2 = txManager.newTransaction();
try {
  t2.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item3")));
  t2.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item4")));
} finally {
  // ※1
  t2.rollback(); // または t2.commit();
  // ※2
  t2.delete();
}

まずは※1のポイントにて。TransactionExamplesテーブルに、Item3, Item4 は入っているものの、ちょっとした鎖付きの情報として扱われているようです。

2013-07-03_1550-td-2

そして、Transactionsテーブルには現在進行中のトランザクション情報が。ちなみに、TransactionImagesには何もありませんでした。

2013-07-03_1553-td-3

次に※2のポイントにて各テーブルを確認すると、TransactionExamplesからはエントリが消えていました。まぁRollbackしたのだから当然ですね。そして、Transactionsテーブルにはまだエントリが残っています。TransactionImagesは相変わらず空っぽです。

では、rollbackではなくcommitした場合、※2のポイントはどうなっているのかというと、大方の予想通り、TransactionExamplesのエントリから鎖が消え、Transactionsテーブルにはまだエントリが残り、そしてTransactionImagesは空っぽ、という結果になります。

しかし、コミット前のデータがTransactionExamplesに入っていることに違和感を感じますよね。しかも変な属性がついている…。ってそれはテーブルを直接見ているからでして。きちんとしたお作法に則った読み出しをすれば、綺麗に見えます。

Transaction t3 = txManager.newTransaction();
Transaction t4 = txManager.newTransaction();
try {
  t3.putItem(new PutItemRequest()
      .withTableName(EXAMPLE_TABLE_NAME)
      .withItem(createItem("Item5")));
  
  GetItemResult t3item5 = t3.getItem(new GetItemRequest(EXAMPLE_TABLE_NAME, createItem("Item5")));
  System.out.println("t3item5 = " + t3item5.getItem());
  GetItemResult t4item5 = t4.getItem(new GetItemRequest(EXAMPLE_TABLE_NAME, createItem("Item5")));
  assert t4item5.getItem() == null;
} finally {
  t3.rollback();
  t4.rollback();
  t3.delete();
  t4.delete();
}

上のように2つのトランザクションを走らせ、t3においてputItemします。その結果、t3.getItemではItem5が見え *1t4.getItemからはItem5が見えない、という結果になります。

いやーー…頑張れば出来るもんですね。トランザクションの実装の中身を眺めたのは実は初めてで、良い経験になりました。

さてさて、ここまでがトランザクションの基本です。あとは更新の競合やエラーハンドリング、読み込みの整合性、分離レベル等、トランザクションというのは非常に話題が豊富です。全部書いているとキリがないのでは、物足りない方は↓からこの本を購入して、弊社のアフィリエイト売り上げに貢献して頂ければ幸いです。

Amazonでチェックする

脚注

  1. しかも、余計な属性はついていない