業務でDynamoDBを使ってきましたが、先日初めて、特定の処理を厳密に実行する必要がでてきたため初めて利用しました。
簡単に利用方法を備忘録として残します。
DynamoDBトランザクションとは?
その名の通り、DynamoDBでトランザクション処理が実行できる機能です。
複数の更新系の処理(PUT, UPDATE, DELETE)、あるいは参照系の処理(GET)をトランザクション内で順次実行し、そのうち1つでも処理が失敗すれば全ての処理をロールバックします。
利用した背景
弊社で作成しているオンラインガチャサービスにて、ガチャの利用上限回数に達した場合はガチャを利用できなくする(ガチャを排出しない)必要がでてきたため、ガチャの抽選(ガチャ結果データの作成)とガチャ利用回数の記録を厳密に記録する必要があったためトランザクションを利用しました。
利用例
今回はNode.jsで利用したのでNode.jsでの利用例になります。
また、書き込みトランザクションのみ利用しています。
要件
- あるガチャを利用した結果を
gacha_results
テーブルに記録する - そのガチャの利用回数と利用上限数が
gacha_draw_limit
テーブルに存在するので、現在の利用回数が上限回数より小さければ、利用回数を1インクリメントする
今回は TransactWriteItem
コマンドを利用します。
TransactWirteItemコマンドは TransactItems
オプションでトランザクション内で実行する処理を複数記述します。
まずはガチャの抽選結果を作成する処理を書きましょう。
作成(上書きも含む)はPutで記述します。
処理の書き方は通常のPutCommandと同じです。
const transactItems = [
{
Put: {
TableName: 'gacha_results',
Item: {
uid: resultUid,
...resultData,
},
ConditionExpression: 'attribute_not_exists(uid)',
}
}
];
uidが同じもの(過去の結果など)に上書きしたくないので、条件付き書き込みを利用しています。
次はコンテンツ利用回数のインクリメント処理です。
アトミックカウンタを使います。
こちらも条件付き更新を利用して、現在の利用回数(total_count)が利用上限回数(limit_count)より小さい時のみインクリメントが成功するようにしています。
transactItems.push(
{
Update: {
TableName: 'content_limit',
Key: { uid: `${content.uid}_${result.id}` },
UpdateExpression: 'SET total_count = total_count + :increment',
ConditionExpression: 'attribute_not_exists(limit_count) OR total_count < limit_count',
ExpressionAttributeValues: { ':increment': 1 },
},
}
);
処理全体は以下のようになりました。
export const ddb = DynamoDBDocumentClient.from(
new DynamoDBClient({
region: 'ap-northeast-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
},
maxAttempts: 3,
});
);
await ddb.send(
new TransactWriteCommand(
TransactItems: [
{
Put: {
TableName: 'content_results',
Item: {
uid: resultUid,
...resultData,
},
ConditionExpression: 'attribute_not_exists(uid)',
},
},
{
Update: {
TableName: 'content_limit',
Key: { uid: `${content.uid}_${result.id}` },
UpdateExpression: 'SET total_count = total_count + :increment',
ConditionExpression: 'attribute_not_exists(limit_count) OR total_count < limit_count',
ExpressionAttributeValues: { ':increment': 1 },
},
}
],
ClientRequestToken: '...'
)
);
これで、ConditionExpressionが満たされないなどの理由でどちらかの処理が失敗すると全ての処理がロールバックされるようになりました。
条件が満たされなかった場合
TransactionCanceledException
エラーが発生します。
どの処理で失敗したか、など詳細は確認できないようです。
TransactionCanceledException: Transaction cancelled, please refer cancellation reasons for specific reasons [None, TransactionConflict]
ConditionCheckについて
Put / Update / Delete 以外にも、 ConditionCheck
という項目をtransactItemsに含めることができます。
何か処理を実行するわけではなく、Transactionで扱う項目以外の項目の存在や値の条件チェックなどができるようです。
当然チェックが失敗すればトランザクションはロールバックされます。
export interface ConditionCheck {
/**
* <p>The primary key of the item to be checked. Each element consists of an attribute name
* and a value for that attribute.</p>
*/
Key: Record<string, AttributeValue> | undefined;
/**
* <p>Name of the table for the check item request.</p>
*/
TableName: string | undefined;
/**
* <p>A condition that must be satisfied in order for a conditional update to
* succeed. For more information, see <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html">Condition expressions</a> in the <i>Amazon DynamoDB Developer
* Guide</i>.</p>
*/
ConditionExpression: string | undefined;
/**
* <p>One or more substitution tokens for attribute names in an expression. For more information, see
* <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html">Expression attribute names</a>
* in the <i>Amazon DynamoDB Developer Guide</i>.</p>
*/
ExpressionAttributeNames?: Record<string, string>;
/**
* <p>One or more values that can be substituted in an expression. For more information, see <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html">Condition expressions</a> in the <i>Amazon DynamoDB Developer Guide</i>.</p>
*/
ExpressionAttributeValues?: Record<string, AttributeValue>;
/**
* <p>Use <code>ReturnValuesOnConditionCheckFailure</code> to get the item attributes if the
* <code>ConditionCheck</code> condition fails. For
* <code>ReturnValuesOnConditionCheckFailure</code>, the valid values are: NONE and
* ALL_OLD.</p>
*/
ReturnValuesOnConditionCheckFailure?: ReturnValuesOnConditionCheckFailure | string;
}
ClientRequestTokenについて
こちらは TransactWriteItem
のオプションとして指定できます。
トランザクションリクエストを冪等にするためにリクエストごとに一意なトークンを指定します。
ネットワークなどの問題などで同じリクエストが再試行されたり複数回送信された場合に、同じリクエストトークンを持つリクエストは2度目以降は実行されません。
つまり同じリクエストトークンを持つリクエストの重複実行を回避してくれるものです。
参考
Amazon DynamoDB Transactions: 仕組み