Djangoでの原子操作?
-
07-07-2019 - |
質問
カウンターの非常に単純なデータモデル(と思う)を実装しようとしています:
class VisitorDayTypeCounter(models.Model):
visitType = models.CharField(max_length=60)
visitDate = models.DateField('Visit Date')
counter = models.IntegerField()
誰かがアクセスすると、visitTypeとvisitDateに一致する行を探します。この行が存在しない場合、counter = 0で作成されます。
次に、カウンターをインクリメントして保存します。
私の懸念は、このプロセスが完全に人種であるということです。 2つの要求は、エンティティが存在するかどうかを同時に確認し、両方で作成できます。カウンターを読み取って結果を保存するまでに、別のリクエストが送信されてインクリメントされる可能性があります(カウントが失われます)。
これまでのところ、Djangoのドキュメントでもチュートリアルでも、これを回避する良い方法を実際に見つけていません(実際、チュートリアルの投票部分に競合状態があるようです)。
これを安全に行う方法
解決
これはちょっとしたハックです。生のSQLはコードの移植性を低下させますが、カウンターの増分で競合状態を取り除きます。理論的には、クエリを実行するたびにカウンタが増加するはずです。これはテストしていませんので、リストがクエリで適切に補間されることを確認する必要があります。
class VisitorDayTypeCounterManager(models.Manager):
def get_query_set(self):
qs = super(VisitorDayTypeCounterManager, self).get_query_set()
from django.db import connection
cursor = connection.cursor()
pk_list = qs.values_list('id', flat=True)
cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list])
return qs
class VisitorDayTypeCounter(models.Model):
...
objects = VisitorDayTypeCounterManager()
他のヒント
Django 1.1以降、ORMのF()式を使用できます。
from django.db.models import F
product = Product.objects.get(name='Venezuelan Beaver Cheese')
product.number_sold = F('number_sold') + 1
product.save()
詳細については、ドキュメントを参照してください:
https://docs.djangoproject .com / en / 1.8 / ref / models / expressions /#django.db.models.F
カウンタを本当に正確にしたい場合は、トランザクションを使用できますが、必要な同時実行の量は、かなりの負荷の下で実際にアプリケーションとデータベースを引き下げます。代わりに、よりメッセージングスタイルのアプローチを検討し、カウンターを増分する訪問ごとにカウントレコードをテーブルにダンプし続けるだけです。次に、訪問の合計数が必要な場合は、訪問テーブルでカウントを行います。また、訪問を合計し、それを親テーブルに格納するバックグラウンドプロセスを1日に何度も実行することもできます。スペースを節約するために、合計した子訪問テーブルからレコードも削除します。同じリソース(カウンター)を争う複数のエージェントがいない場合、同時実行コストを大幅に削減できます。
http://code.djangoproject.com/ticket/2705データベースレベルのロックをサポート。
パッチを使用すると、このコードはアトミックになります:
visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()
2つの提案:
unique_togetherをモデルに追加し、作成を例外ハンドラーでラップして重複をキャッチします:
class VisitorDayTypeCounter(models.Model):
visitType = models.CharField(max_length=60)
visitDate = models.DateField('Visit Date')
counter = models.IntegerField()
class Meta:
unique_together = (('visitType', 'visitDate'))
この後、カウンタの更新で小さな競合状態が発生する可能性があります。それを心配するのに十分なトラフィックが得られたら、よりきめ細かいデータベース制御のためにトランザクションを調べることをお勧めします。 ORMがロック/同期を直接サポートしているとは思わない。トランザクションのドキュメントはこちらで入手できます。
データベースを同時実行レイヤーとして使用しないのはなぜですか?テーブルの主キーまたは一意制約をvisitTypeおよびvisitDateに追加します。間違っていなければ、djangoはデータベースのModelクラスでこれを正確にサポートしていないか、少なくとも例を見ていません。
テーブルに制約/キーを追加したら、必要な作業は次のとおりです。
- 行があるかどうかを確認します。ある場合は、フェッチします。
- 行を挿入します。エラーがなければ問題はなく、先に進むことができます。
- エラー(競合状態など)がある場合は、行を再フェッチします。行がない場合、それは真のエラーです。それ以外は大丈夫です。
この方法で行うのは厄介ですが、それは十分に高速で、ほとんどの状況をカバーするようです。
この種の競合状態を回避するには、データベーストランザクションを使用する必要があります。トランザクションを使用すると、「all or nothing」のカウンターを作成、読み取り、インクリメント、保存する操作全体を実行できます。ベース。何か問題が発生した場合は、すべてがロールバックされ、再試行できます。
Django ドキュメントをご覧ください。トランザクションミドルウェアがあります。または、ビューまたはメソッドの周りにデコレーターを使用してトランザクションを作成できます。