0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DynamoDBのConditionExpressionで更新競合を防ぐ:Javaで楽観ロックを実装する

0
Posted at

DynamoDBで同じアイテムを複数の処理が同時に更新すると、後から書いた処理が先の変更を上書きする「更新消失」が起きることがあります。

この対策に使えるのが、ConditionExpressionを使った楽観ロックです。楽観ロックは「通常は競合しない前提で処理し、書き込み時だけデータが途中で変わっていないか確認する」方法です。

version属性で競合を検出する

次の注文を2つの処理が同時に読んだとします。

{
  "orderId": "order-001",
  "status": "PENDING",
  "version": 7
}

両方がversion = 7を読んでも、書き込み時に「まだversionが7なら更新する」という条件を付ければ、先に成功した処理だけがversionを8へ進められます。

AWS SDK for Java 2.xで実装する

import java.util.Map;

import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ConditionalCheckFailedException;
import software.amazon.awssdk.services.dynamodb.model.ReturnValuesOnConditionCheckFailure;
import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest;

public void updateStatus(
        DynamoDbClient dynamoDbClient,
        String orderId,
        String newStatus,
        long expectedVersion) {

    var request = UpdateItemRequest.builder()
            .tableName("Orders")
            .key(Map.of(
                    "orderId",
                    AttributeValue.builder().s(orderId).build()))
            .updateExpression(
                    "SET #status = :newStatus, #version = :nextVersion")
            .conditionExpression(
                    "#version = :expectedVersion")
            .expressionAttributeNames(Map.of(
                    "#status", "status",
                    "#version", "version"))
            .expressionAttributeValues(Map.of(
                    ":newStatus",
                    AttributeValue.builder().s(newStatus).build(),
                    ":expectedVersion",
                    AttributeValue.builder()
                            .n(Long.toString(expectedVersion))
                            .build(),
                    ":nextVersion",
                    AttributeValue.builder()
                            .n(Long.toString(expectedVersion + 1))
                            .build()))
            .returnValuesOnConditionCheckFailure(
                    ReturnValuesOnConditionCheckFailure.ALL_OLD)
            .build();

    try {
        dynamoDbClient.updateItem(request);
    } catch (ConditionalCheckFailedException e) {
        throw new IllegalStateException(
                "別の処理が先に更新しています。最新データを再取得してください。",
                e);
    }
}

重要なのは次の条件です。

#version = :expectedVersion

条件がtrueなら更新し、falseならDynamoDBはConditionalCheckFailedExceptionを返します。条件判定と更新を同じ書き込み操作で行えるため、更新直前の競合を検出できます。

ReturnValuesOnConditionCheckFailureを使う

ReturnValuesOnConditionCheckFailure.ALL_OLDを指定すると、条件に失敗した時点のアイテムを例外から取得できます。

catch (ConditionalCheckFailedException e) {
    if (e.hasItem()) {
        var currentVersion = e.item().get("version");
        System.out.println(currentVersion.n());
    }
}

AWS公式では、この返却のために追加のRead Capacity Unitは消費しません。競合後に現在値を確認するための追加GetItemを減らせます。

新規作成はattribute_not_exists

PutItemは同じ主キーがあると既存アイテムを置き換えます。新規作成だけを許可したい場合は次の条件を使います。

attribute_not_exists(orderId)

作成時はattribute_not_exists、更新時はversion確認、と役割を分けると意図しない上書きを防ぎやすくなります。

Enhanced Clientなら自動化できる

AWS SDK for Java 2.xのDynamoDB Enhanced Clientでは、version属性に@DynamoDbVersionAttributeを付けるとVersionedRecordExtensionが楽観ロックを処理します。

@DynamoDbVersionAttribute
public Integer getVersion() {
    return version;
}

標準設定では、新規アイテムのversionは0から始まり、更新成功時に1ずつ増えます。

独自ExtensionをEnhanced Clientへ設定すると、デフォルトExtensionは自動読み込みされません。楽観ロックを残す場合はVersionedRecordExtensionも明示的に追加します。

Deleteは別扱い

AWS SDK for Java 2.xでは、@DynamoDbVersionAttributeを付けてもDelete時にはversion条件が自動追加されません。

削除でも競合を検出したい場合は、DeleteへConditionExpressionを明示的に設定します。

失敗した条件付き書き込みにもコストはかかる

条件がfalseになった場合でも、条件付き書き込みはWrite Capacityを消費します。

ConditionalCheckFailedExceptionは単なる通信失敗ではなく「期待していた状態と現在の状態が違う」という競合です。同じ古いversionのまま無条件に再試行せず、最新状態を確認してから再判定します。

一般的には、

  1. 最新データを確認する
  2. 業務上まだ更新してよいか判定する
  3. 新しいversionを使って再実行する

という流れにします。

Global Tablesでは注意

DynamoDB Global Tablesはリージョン間の競合を「last writer wins」で調整します。そのため、単一リージョンで使うversionベースの楽観ロックと同じ前提では動かない場合があります。

まとめ

DynamoDBの更新競合対策では、書き込み自体に条件を持たせるのが重要です。

  • 新規作成はattribute_not_exists
  • 更新はversionをConditionExpressionで確認
  • 競合はConditionalCheckFailedExceptionとして処理
  • ReturnValuesOnConditionCheckFailureで現在値を受け取れる
  • Enhanced Clientでは@DynamoDbVersionAttributeで自動化できる
  • DeleteとGlobal Tablesは挙動に注意する

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?