質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.25%

Djangoチュートリアルにおける競合状態について

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 2,430

study

score 61

取り組んでいること

公式ドキュメント「はじめての Django アプリ作成、その 4」の「簡単なフォームを書く」について、以下の動作を試作しています。

以下の選択を表示し、ラジオボタン選択後Voteをクリック → 次ページに移動し集計結果を表示
イメージ説明 

イメージ説明

この機能の問題点について、チュートリアルに以下の説明がありました。(わかりやすくするためvoteの数を10としました)

(注釈)これまで作ってきた vote() ビューのコードは、小さな問題を抱えています。最初にデータベースから selected_choice オブジェクトを取得し、そこで votes の新しい値を計算し、データベースにそれを戻して保存します。もしウェブサイトのユーザー 2 人が まったく同時に 投票しようとすると、誤りが発生します。votes の元の値が 10 だったとしましょう。その時、両方のユーザーに対して新しい値として 11 が計算され保存されます、しかし 12 が本来想定される値です。
この問題は、「競合状態」と呼ばれています。この問題に興味がある人は、Avoiding race conditions using F() を読むと、この問題の解決方法がわかります。

上記のような「競合状態」について、3点疑問があります。

疑問1

上記の問題はvoteの値である「10」をpythonが直接読み込んでしまうことが要因のように思います。voteの値に1を加えたいだけであれば、DBに対して直接「現在のvoteの値に1を加えろ」という命令を出すことができれば、問題が無いように思います。この考え方は間違っていますでしょうか?
なお、上記のvoteの値である「10」をpythonが直接読み込んで1を加えて返すという、問題のコードは以下のとおりです」

polls/views.py
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    selected_choice = question.choice_set.get(pk=request.POST['choice']
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(redirect_to=reverse(viewname='polls:results', args=(question.id,)))

疑問2

この問題への対応として、チュートリアルの注釈ではF()を使えとあります。疑問1で考えたように、python上で処理せずにDBに対して直接「現在のvoteの値に1を加えろ」という命令を出す仕組み」は、このF()を使った方法で実現できるのでしょうか?
F()を使って自分で書いた以下のコードでは、「selected_choice.votes = F('votes') + 1」の部分でpython上にvoteの値を読み込んでしまっており、次の行の「selected_choice.save()」が実行されるまでに他のユーザーが同じ操作をしてしまうと、結局同じ問題が発生するように見えます。しかし、実際は見かけとは異なり、python上に具体的にvoteの「10」という値が読み込まれるのではなく、一連の動作を変換して、DBへ「現在のvoteの値に1を加えろ」という命令を行ってくれているという認識で良いでしょうか?

polls/views.py
def vote(request, question_id):
    from django.db.models import F
    question = get_object_or_404(Question, pk=question_id)   
    selected_choice = question.choice_set.get(pk=request.POST['choice'])
    selected_choice.votes = F('votes') + 1
    selected_choice.save()
    return HttpResponseRedirect(redirect_to=reverse(viewname='polls:results', args=(question.id,)))

疑問3

今回、「競合状態」という言葉を調べるにあたって、悲観的ロック、楽観的ロックというDB操作に関わる用語を知りました。
voteの値を正しく更新する事が目的の場合、上記のF()を使用していればいずれのロックも不要でしょうか?
※ 逆にF()を使っているにも関わらず、ロックが必要となるのはどのような場合でしょうか?

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+3

疑問1: DB に対し votes = votes + 1 と実行さればよい

認識の通りです。この現象は一般的にロストアップデートと呼ばれています。今回のように相対的な表現で記述したり、サブクエリを使うなどして1回のSQL文で処理を完結出来る場合、データベースサーバー側で適切な排他処理を行うためロストアップデートは起きません。


疑問2:Django データモデルの F() expressions

これも認識の通りです。F() を使った場合、Django 側はこの値を取得したり加工することはしません。save() を発行したときに、UPDATE "question" SET "vote" = ("question"."votes" + 1) といった感じのSQL文が発行されます。

ちなみにアップデートしたあとの値を Django で利用したい場合は、question.refresh_from_db() としてデータベースから再度値を取得する必要があります。


疑問3:ロックの必要性

今回のケースでは、F() を使って相対的な加算を行えばロストアップデートは起きませんのでロックは不要です。

考慮が必要になってくるのはデータベースと複数回やりとりする場合です。よくある例えに銀行の振り込み処理があります。

  1. Aさんの口座が有効で、かつ残高が50万円以上あることを確認
  2. Bさんの口座が有効であることを確認
  3. Aさんの口座から50万円を引く
  4. Bさんの口座に50万円を足す

まず、これらの処理が途中で止まってしまうと大変なことになってしまいますよね。そこでこれらの処理を絶対にひとまとめにして処理させる仕組みであるトランザクション を使います。

さらに処理中にAさんの口座からお金が引き落とされて残高がマイナスになったりしないよう、Aさんの口座残高に対して排他ロックをかけたりします。

Django の ORM はトランザクションも排他ロックも取り扱えますが、そもそもデータベースの基礎知識が必要になってくるので、厳格な同時アクセス制御を行う場合は別途学習することをおすすめします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/07/23 20:45

    とてもわかり易い解説をありがとうございました。
    手元の入門書にはロックの説明がなく、トランザクションの説明も少しだけだったため、教えて頂き勉強になりました。
    DBについてもう少し学習が必要なようなので、より詳しい本を探してみます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.25%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る