0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Django】 select_for_update() で俺たちの時間を絶対に守ろう(排他制御)

Posted at

はじめに

今日は「並行アクセスの悪夢」から我々のデータを守る究極の武器、select_for_update()について語ろうではないか。複数ユーザーが同時に同じレコードを更新する恐怖。そう、それはまるで一つのケーキを奪い合う5歳児たちのようなカオスを生み出すのである。

select_for_update()とは何か

select_for_update()とは、Djangoが提供する強力な呪文である。これを唱えることにより、選択したレコードに「触るな、今は俺の時間だ」という標識を立てることができる。技術的には、これはデータベースの行レベルロックを取得するSQLのSELECT FOR UPDATE句を実行するメソッドである。

# 他の開発者に宣言する:「このユーザーは今、私が占有している!」
with transaction.atomic():
    user = User.objects.select_for_update().get(id=42)
    user.last_login = timezone.now()
    user.save()
    # ロックは自動的に解放される。さあ、次の戦士よ、前へ!

実践的な実装パターン

基本パターン:シンプルなロック取得

from django.db import transaction

def update_user_points(user_id, points_to_add):
    with transaction.atomic():
        # ここで魔法が始まる
        user = User.objects.select_for_update().get(id=user_id)
        
        # 安全地帯で作業開始
        user.points += points_to_add
        user.save()
        
        return user.points

待機しない猛者向けパターン

「待つのは時間の無駄だ」と考える者には、nowait=Trueオプションがある。これにより、ロックが取得できない場合は即座に例外が発生する。まるでコーヒーショップで「並ぶ気はない。席が空いてなければ別の店に行く」という強気の客のようなものだ。

from django.db import transaction, DatabaseError

def update_stock_nowait(product_id, quantity):
    try:
        with transaction.atomic():
            # 待つなんてしない。今すぐにロックを取得するか、さもなくば去る
            product = Product.objects.select_for_update(nowait=True).get(id=product_id)
            
            if product.stock < quantity:
                return False
                
            product.stock -= quantity
            product.save()
            return True
            
    except DatabaseError:
        # 他のトランザクションがロックを持っている場合
        return False  # 「また後で来ます」

複数テーブルにまたがる複雑な取引

複数のモデルをロックする必要がある場合、順序が重要である。デッドロックはプログラマーが夜中に冷や汗をかく原因となる。

def transfer_money(from_account_id, to_account_id, amount):
    # 常に小さいIDから大きいIDの順にロックを取得することでデッドロックを回避
    if from_account_id > to_account_id:
        from_account_id, to_account_id = to_account_id, from_account_id
        is_reversed = True
    else:
        is_reversed = False
        
    with transaction.atomic():
        from_account = Account.objects.select_for_update().get(id=from_account_id)
        to_account = Account.objects.select_for_update().get(id=to_account_id)
        
        # 向きを戻す
        if is_reversed:
            from_account, to_account = to_account, from_account
            
        if from_account.balance < amount:
            raise InsufficientFunds("銀行強盗でもしない限り、無いものは送れない")
            
        from_account.balance -= amount
        to_account.balance += amount
        
        from_account.save()
        to_account.save()
        
        # トランザクション履歴を記録
        Transfer.objects.create(
            from_account=from_account,
            to_account=to_account,
            amount=amount,
            description="スムーズな資金移動。Djangoの排他制御に感謝を"
        )

高度なテクニック

特定のテーブル列のみをロック

完全な力を解放したくない場合は、特定の列だけをロックすることも可能だ。

# PostgreSQLの場合
from django.db import transaction

def update_specific_fields(product_id):
    with transaction.atomic():
        # 在庫と価格のみをロック
        product = Product.objects.select_for_update(of=('self',)).get(id=product_id)
        
        # ビジネスロジック...

タイムアウト付きロック

「永遠に待ちたくはない」という賢明な態度のために、タイムアウトを設定できる(PostgreSQL限定の機能)。

from django.db import transaction, OperationalError

def update_with_timeout(user_id):
    try:
        with transaction.atomic():
            # 3秒待っても駄目なら諦める
            user = User.objects.select_for_update(timeout=3).get(id=user_id)
            # 処理...
            
    except OperationalError:
        return "時は金なり。これ以上待てぬ。"

実戦での注意点

  1. トランザクション内でのみ使用可能 - トランザクションの外で使うとDjangoは「それは魔法の杖を逆さに持っているようなものだ」と笑うだろう。

  2. パフォーマンスへの影響 - ロックは強力だが、使いすぎるとアプリケーションは亀のように遅くなる。適切な粒度で使おう。

  3. デッドロックの危険 - 複数のロックを取得する場合は、常に同じ順序で行うこと。さもなければ、デッドロックという名の迷宮に迷い込むことになる。

  4. テスト環境での注意 - SQLiteはselect_for_update()をサポートしていない。テスト環境と本番環境で動作が異なる可能性に注意せよ。

結論

select_for_update()は、並行アクセス問題に立ち向かう強力な武器である。賢く使えば、データの整合性を保ちながら、ユーザーに「なぜこの更新が消えたんだ!」と叫ばせることもない。

排他制御は技術であると同時に芸術でもある。力強く、しかし繊細に。広く、しかし必要な場所だけに。そして何よりも、コードレビューで同僚に「君は排他制御の真髄を理解している」と言わせることができれば、真の勝利と言えるだろう。

データベースとの戦いで幸運を祈る!そして覚えておいて欲しい。

「ロックを取ったら、必ず解放せよ。さもなければ、君のアプリケーションは永遠の眠りにつくだろう。」

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?