【DynamoDB/Boto3入門】DynamoDBでアトミックカウンタを実装しつつ更新式について学ぶ。

記事タイトルとURLをコピーする

こんにちは、技術一課の加藤です。先日、特定条件のファイルの個数をカウントするために、DynamoDBを使ったアトミックカウンタを実装しました。 こりゃー便利だと思うと同時に、DynamoDBのUpdateItemについてある程度わかっていないとちょっとだけ困るなあと感じたので、実装手順や注意点を備忘録的に書き残しておきます。

アトミックカウンタ

他の書き込みリクエストを妨害することなく、数値をカウントアップ/ダウンするようなカウンタのことをアトミックカウンタと呼びます。 例えばアクセスカウンタのような、1回の操作につき必ず1ずつカウントアップするような仕組みを考えるとします。普通に考えると、

  1. SELECTして現在の値を取得する
  2. 取得した値に1を足して、UPDATEで値を更新する

という2ステップが必要になりますがトランザクション管理をしっかりやっていないと、値の取得をしている間に別のクライアントが値の更新をしてしまった場合など不具合が生じる可能性があり、あまり良い方法ではありません。 この状況を回避し、アトミックカウンタを実装するために使うのがUpdateItemの更新式という仕組みです。

UpdateItemの更新式(UpdateExpression)

公式情報はこちら。更新式 - Amazon DynamoDB キーを指定してアップデートをかけるとき、通常はアップデートする値を指定しますが、更新式を使うと式を指定することができます。
更新したい項目のキーと式を指定すると、式を計算をして値をアップデートしてくれます。

Boto3によるアトミックカウンタの実装

言葉で説明するだけだとわかりにくいので実際のコードを見てみましょう。今回はAWS SDK for Python(Boto3)を利用していきます。 なお、今回更新対象とするテーブルは、話を単純にするために

  • Date(String) - Primary Key
  • TestCounter(Number)

の2つの属性のみを持つ簡易なものを使用します。

import boto3

from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('AtomicCounter')

now = datetime.now()
date_str = now.strftime('%Y-%m-%d')

# 今日のTestCounterをカウントアップする
table.update_item(
    Key={
        'Date': date_str
    },
    UpdateExpression='ADD TestCounter :incr',
    ExpressionAttributeValues={
        ':incr': 1
    }
)

更新式の指定はUpdateExpressionという引数をupdate_itemに渡すことで行います。 このコードを実行すると、TestCounterが1ずつカウントアップされることを確認できます。ExpressionAttributeValuesの:incrの値を3や5など適当な数値に変更すると、指定した数カウントアップされるので試してみてください。

複数の値を更新したい場合

ある属性をカウントアップしつつ、別の属性の値を変更したいこともあるかと思います。例えばフラグを書き換えるなどの動作が想定されますが、これもUpdateExpressionで行うことができます。 逆にいえば、式を使わない通常の値の更新時に使うAttributeUpdatesという引数とUpdateExpressionは併用できないためUpdateExpressionのみで複数の値の更新を実装することになります。 ここではTestCounterのカウントアップと同時に以下の動作を行うコードを例示します。

  • TestFlag(Bool)をFalseにする
  • TestTime(String)を更新する
import boto3

from datetime import datetime

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('AtomicCounter')

now = datetime.now()
date_str = now.strftime('%Y-%m-%d')
time_str = now.strftime('%H:%M:%S')

# 今日のcounterをカウントアップする
table.update_item(
    Key={
        'Date': date_str
    },
    UpdateExpression="""
        ADD TestCounter :incr
        SET TestFlag = :flag, TestTime = :time
    """,
    ExpressionAttributeValues={
        ':incr': 1,
        ':flag': False,
        ':time': time_str
    }
)

このように、

  • Action(SET, ADD, REMOVE,DELETE)が同じ処理はカンマ区切り
  • Actionが異なる処理はスペースや改行区切り

で書くことになります。

SETとADDの違い

アトミックカウンタの実装例はネット上に色々転がっているのですが、大抵SET句を用いたパターンとADD句を用いたパターンの2通りが出てきます。 公式ページには 、

通常は、SETではなくADDを使用することをお勧めします。

とADDを推奨する記載がありますし、個人的にもアトミックカウンタを実装するのであればADDを使う方がいいのではないかと考えています。 SET句でカウンタを実装する場合、UpdateExpressionは以下のようになります。

SET TestCounter = TestCounter + :incr

こうすると、対象レコードにTestCounterがもともと含まれていない場合エラーが発生します。エラーを解消するためには、初期値を別の手段でセットしてあげる必要がありやや手間です。 ADD句の場合、対象レコードに存在しない属性を指定すると勝手に初期値を0として計算してくれます。AttributeUpdates - Amazon DynamoDB

ADDを使用して、更新前に存在しない項目の数値を増やすまたは減らす場合、DynamoDB は初期値として 0 を使用します。

このため今回例示したコードのようにレコードが新しく作られるような実装をする場合は、ADDを使った方が楽に実装が可能です。