ジャンゴのレースコンディション
-
06-07-2019 - |
質問
競合状態の可能性があるジャンゴビューの簡単な例を次に示します。
# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points
def add_points(request):
user = request.user
user.points += calculate_points(user)
user.save()
競合状態はかなり明白なはずです。ユーザーはこのリクエストを2回行うことができ、アプリケーションは user = request.user
を同時に実行する可能性があり、リクエストの1つが他のリクエストをオーバーライドする可能性があります。
関数 calculate_points
が比較的複雑で、単一の update
に入れることができずに入れるのが難しい、あらゆる種類の奇妙なものに基づいて計算をするとします。ストアドプロシージャ。
だからここに私の質問があります:これに似た状況に対処するために、どのようなロックメカニズムがdjangoで利用可能ですか?
解決
Django 1.4+は select_for_update 、以前のバージョンでは、生のSQLクエリを実行できます。 select ... for update
は、基盤となるDBに応じて、更新からの行をロックします。トランザクションの終了まで、その行で何でもできます。例:
from django.db import transaction
@transaction.commit_manually()
def add_points(request):
user = User.objects.select_for_update().get(id=request.user.id)
# you can go back at this point if something is not right
if user.points > 1000:
# too many points
return
user.points += calculate_points(user)
user.save()
transaction.commit()
他のヒント
Django 1.1以降、ORMのF()式を使用してこの特定の問題を解決できます。
from django.db.models import F
user = request.user
user.points = F('points') + calculate_points(user)
user.save()
詳細については、ドキュメントを参照してください:
https://docs.djangoproject .com / en / 1.8 / ref / models / expressions /#django.db.models.F
データベースのロックはここへ行く方法です。 「更新の選択」を追加する計画があります。 Djangoのサポート(こちら)が、今のところ最も簡単なのは生のSQLを使用して更新することです。スコアの計算を開始する前のユーザーオブジェクト。
ペシミスティックロックは、基礎となるDB(Postgresなど)がサポートする場合、Django 1.4のORMでサポートされるようになりました。 Django 1.4a1リリースノートを参照してください。 。
この種のものをシングルスレッド化する方法はたくさんあります。
標準的なアプローチの1つは、最初に更新です。行の排他ロックを捕捉する更新を行います。その後、あなたの仕事をします。そして最後に変更をコミットします。これが機能するには、ORMのキャッシュをバイパスする必要があります。
別の標準的なアプローチは、複雑な計算からWebトランザクションを分離する、個別のシングルスレッドアプリケーションサーバーを持つことです。
-
Webアプリケーションは、スコアリングリクエストのキューを作成し、別のプロセスを生成して、スコアリングリクエストをこのキューに書き込むことができます。スポーンはDjangoの
urls.py
に配置できるため、Webアプリの起動時に発生します。または、別のmanage.py
管理スクリプトに入れることもできます。または、「必要に応じて」行うことができます。最初のスコアリング要求が試行されたとき。 -
Werkzeugを使用して、urllib2を介してWSリクエストを受け入れる別のWSGIフレーバーWebサーバーを作成することもできます。このサーバーに単一のポート番号がある場合、要求はTCP / IPによってキューに入れられます。 WSGIハンドラーに1つのスレッドがある場合、シリアル化されたシングルスレッドを実現しています。スコアリングエンジンはWSリクエストであり、どこでも実行できるため、これはわずかにスケーラブルです。
さらに別のアプローチは、計算を実行するために取得して保持する必要のある他のリソースを持つことです。
-
データベース内のシングルトンオブジェクト。一意のテーブルの単一の行をセッションIDで更新して、制御を獲得できます。セッションID
None
で更新して、コントロールを解放します。不可欠な更新には、WHERE SESSION_ID IS NONE
フィルターを含めて、他の誰かがロックを保持しているときに更新が失敗するようにする必要があります。これは本質的にレースフリーであるため、興味深いものです-それは単一の更新であり、SELECT-UPDATEシーケンスではありません。 -
園芸品種のセマフォは、データベースの外部で使用できます。キューは(一般に)低レベルのセマフォよりも簡単に操作できます。
これはあなたの状況を単純化しすぎているかもしれませんが、JavaScriptリンクの置き換えはどうですか?言い換えると、ユーザーがリンクまたはボタンをクリックすると、すぐに無効化/「グレーアウト」するJavaScript関数でリクエストがラップされます。リンクし、テキストを" Loading ..."に置き換えます。または「リクエストを送信しています...」情報など。それはあなたのために働きますか?
今、使用する必要があります:
Model.objects.select_for_update().get(foo=bar)