カウンターをカウントし、カウントを失わない方法







ブログの購読者の数。 公開されたユーザー投稿の数。 コメントに対する賛成票と反対票の数。 有料製品注文の数。 このようなことを考えたことはありますか? その後、定期的に失くしたに違いない。 さあ、連絡先も失った:













私はあなたのことは知りませんが、私の人生では、カウンターはキャッシュの無効化と命名の後のほとんど最初の問題です。 私はそれを完全に決定したとは言いません。 Habr、Gif〜Gift、Durty、Tripsterなどのプロジェクトに取り組んでいる間に思いついたアプローチをコミュニティと共有したいだけです。 これが時間と神経細胞の節約に役立つことを願っています。







カウンターを誤ってカウントする方法



カウンターに対する最も一般的な2つの間違ったアプローチから始めます。







  1. 変更が発生する可能性のあるすべての場所(作成、編集、公開、投稿の公開、モデレーターによる削除、管理パネルでの変更など)でカウンター値を増分/増減します。







  2. 関連付けられているオブジェクトが変更されるたびに、カウンターを完全に再カウントします。


また、これらのアプローチのさまざまな組み合わせ(たとえば、適切な場所で増分し、1日に1回、完全にバックグラウンドでカウントします)。 これらのアプローチが間違っているのはなぜですか? 要するに、答えは次のとおりです。私は試しましたが、成功しませんでした。







しかし、何が正しいのでしょうか?



確かに、記事に記載されている方法だけではありません。 しかし、私は2つの重要な原則に到達しました。私見では、それらはすべての「正しい」方法に適用できます。







  1. 1つのカウンターの更新は1か所で行う必要があります。







  2. 更新時には、変更前と変更後のオブジェクトの状態を知る必要があります。


次のセクションは、私が彼らにどうやって来たかを説明する試みです。 一貫して、出版カウンターのより複雑な要件の例によって、段階的に。 説明では、Pythonで擬似コードを使用します。







数式の検索:単純なものから複雑なものまで



最も簡単なオプション。 作成されたすべての投稿にカウンターが必要です。







@on('create_post') def update_posts_counter_on_post_create(post): posts_counter.update(+1) @on('delete_post') def update_posts_counter_on_post_delete(post): posts_counter.update(-1)
      
      





ここで、プロジェクトに「ドラフト」の概念を導入し、ユーザーが未完成の投稿を保存して後でHabréのように変更できるようにします。 すべてではなく、公開された投稿のみをカウントする条件をカウンターに追加します。







 @on('create_post') def update_posts_counter_on_post_create(post): if post.is_published: posts_counter.update(+1) @on('delete_post') def update_posts_counter_on_post_delete(post): if post.is_published: posts_counter.update(-1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): if post_old.is_published != post_new.is_published: #   , #       if post_new.is_published: posts_counter.update(+1) else: posts_counter.update(-1)
      
      





さらに、リカバリの可能性なしにデータベースから投稿を削除するのは悪いことだと理解しています。 代わりに、 is_deleted



フラグを追加します。 もちろん、削除された投稿もカウントされるべきではありません。







 @on('create_post') def update_posts_counter_on_post_create(post): if post.is_published and not post.is_deleted: update_posts_counter(+1) @on('delete_post') def update_posts_counter_on_post_delete(post): if post.is_published and not post.is_deleted: update_posts_counter(-1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): is_published_changed = post_old.is_deleted != post_new.is_deleted is_deleted_changed = post_old.is_deleted != post_new.is_deleted #  /  if is_published_changed and not is_deleted_changed: if post_new.is_published: update_posts_counter(+1) else: update_posts_counter(-1) #  /  if not is_deleted_changed and not is_published_changed: if post_new.is_deleted: update_posts_counter(-1) else: update_posts_counter(+1) #    ,        if is_published_changed and is_deleted_changed: pass
      
      





コードはすでにかなり混乱しています...それでも、プロジェクトにマルチブログを追加しています。

blog_id



フィールドは投稿に表示されますが、ブログの場合は自分の投稿カウンターが欲しいです

(自然に公開され、削除されていない)。 同時に、あるブログから別のブログに投稿を移動する可能性を検討する価値があります。 合計投稿カウンターについては忘れます。







 @on('create_post') def update_posts_counter_on_post_create(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, +1) @on('delete_post') def update_posts_counter_on_post_delete(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, -1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): #    ,    if post_old.blog_id == post_new.blog_id: is_published_changed = post_old.is_deleted != post_new.is_deleted is_deleted_changed = post_old.is_deleted != post_new.is_deleted #  /  if is_published_changed and not is_deleted_changed: if post_new.is_published: update_posts_counter(post_new.blog_id, +1) else: update_posts_counter(post_new.blog_id, -1) #  /  if not is_deleted_changed and not is_published_changed: if post_new.is_deleted: update_posts_counter(post_new.blog_id, -1) else: update_posts_counter(post_new.blog_id, +1) #     else: if post_old.is_published and not post_old.is_deleted: update_blog_post_counter(post_old.blog_id, -1) if post_new.is_published and not post_new.is_deleted: update_blog_post_counter(post_new.blog_id, +1)
      
      





いいね つまり うんざり! ブログ投稿の数だけでなく、各ユーザーのブログ投稿の数[user_id、post_id]→post_countをカウントするカウンターについても考えたくありません。 そして、例えば、ユーザープロファイルに統計を表示するためにそれらを必要としました...







しかし、あるブログから別のブログに投稿を移動するためのコードに注意しましょう。 突然、それはよりシンプルで短いことが判明しました。 さらに、作成/削除コードと非常によく似ています! 実際、これは起こっています。古いブログから投稿を削除し、新しいブログを作成しています。 ブログが同じままの場合、同じ原則を適用できますか? はい







 @on('create_post') def update_posts_counter_on_post_create(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, +1) @on('delete_post') def update_posts_counter_on_post_delete(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, -1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): if post_old.is_published and not post_old.is_deleted: update_blog_post_counter(post_old.blog_id, -1) if post_new.is_published and not post_new.is_deleted: update_blog_post_counter(post_new.blog_id, +1)
      
      





唯一のマイナスは、投稿を保存するたびに、カウンターが2回更新されることです。 さらに、ほとんどの場合無駄になります。 最初にカウンターの増分をカウントしてから、必要に応じて更新しますか?







 @on('create_post') def update_posts_counter_on_post_create(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, +1) @on('delete_post') def update_posts_counter_on_post_delete(post): if post.is_published and not post.is_deleted: update_blog_post_counter(post.blog_id, -1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): increments = defaultdict(int) if post_old.is_published and not post_old.is_deleted: increments[post_old.blog_id] -= 1 if post_new.is_published and not post_new.is_deleted: increments[post_new.blog_id] += 1 for blog_id, increment in increments.iteritems(): if increment: update_blog_post_counter(blog_id, increment)
      
      





すでにはるかに優れています。 次に、 counter_value



関数を作成して、 post.is_published and not post.is_deleted



重複を取り除きましょう。 考慮される投稿に対して1を返し、削除または公開された投稿に対して0を返します。







 counter_value = lambda post: int(post.is_published and not post.is_deleted) @on('create_post') def update_posts_counter_on_post_create(post): if counter_value(post): update_blog_post_counter(post.blog_id, +1) @on('delete_post') def update_posts_counter_on_post_delete(post): if counter_value(post): update_blog_post_counter(post.blog_id, -1) @on('change_post') def update_posts_counter_on_post_change(post_old, post_new): increments = defaultdict(int) increments[post_old.blog_id] -= counter_value(post_old) increments[post_new.blog_id] += counter_value(post_new) for blog_id, increment in increments.iteritems(): if increment: update_blog_post_counter(blog_id, increment)
      
      





これで、作成/変更/削除イベントを1つにまとめる準備ができました。 作成/削除するときは、パラメーターpost_old



/ post_new



のいずれかではなく、単にNone



post_new



渡します。







 @on('change_post') def update_posts_counter_on_post_change(post_old=None, post_new=None): counter_value = lambda post: int(post.is_published and not post.is_deleted) increments = defaultdict(int) if post_old: increments[post_old.blog_id] -= counter_value(post_old) if post_new: increments[post_new.blog_id] += counter_value(post_new) for blog_id, increment in increments.iteritems(): if increment: update_blog_post_counter(blog_id, increment)
      
      





いいね! 各ユーザーのブログ投稿のカウントに戻ります。 今では非常に簡単であることがわかりました。







 @on('change_post') def update_posts_counter_on_post_change(post_old=None, post_new=None): counter_value = lambda post: int(post.is_published and not post.is_deleted) increments = defaultdict(int) if post_old: increments[post_old.user_id, post_old.blog_id] -= counter_value(post_old) if post_new: increments[post_new.user_id, post_new.blog_id] += counter_value(post_new) for (user_id, blog_id), increment in increments.iteritems(): if increment: update_user_blog_post_counter(user_id, blog_id, increment)
      
      





上記のコードは、必要に応じて出版物の著者の変更を考慮に入れていることに注意してください。 また、他のパラメーターのアカウンティングを追加するのも簡単です。 increments



用の新しいキーを追加するだけです。







先に進みます。 本格的なマルチブログプラットフォームでは、おそらく出版物の評価が表示されています。 投稿数だけでなく、各ブログの各ユーザーの総合評価を考慮して、「最高の著者」を表示するとします。 counter_value



して、1/0ではなく投稿の評価(公開されている場合)を返し、それ以外の場合は0を返すようにします。







 @on('change_post') def update_posts_counter_on_post_change(post_old=None, post_new=None): counter_value = lambda post: post.rating if (post.is_published and not post.is_deleted) else 0 increments = defaultdict(int) if post_old: increments[post_old.user_id, post_old.blog_id] -= counter_value(post_old) if post_new: increments[post_new.user_id, post_new.blog_id] += counter_value(post_new) for (user_id, blog_id), increment in increments.iteritems(): if increment: update_user_blog_post_counter(user_id, blog_id, increment)
      
      





ユニバーサルフォーミュラ



要約すると、ユニバーサルカウンターの抽象式は次のとおりです。







 @on('change_obj') def update_some_counter(obj_old=None, obj_new=None): counter_key = lambda obj: ... counter_value = lambda obj: ... if obj_old: increments[counter_key(obj_old)] -= counter_value(obj_old) if obj_new: increments[counter_key(obj_new)] += counter_value(obj_new) for counter_key, increment in increments.iteritems(): if increment: update_counter(counter_key, increment)
      
      





最後に



軟膏でハエなしでいかに! 上記の式は理想的ですが、球形の真空からそれを厳しい現実に移すと、カウンターは迷う可能性があります。 これは、次の2つの理由で発生します。







  1. オブジェクトを変更するすべての可能なシナリオをインターセプトすることは、実際には簡単な作業ではありません。 作成/変更/削除のシグナルを提供するORMを使用し、オブジェクトの古い状態を保持する自転車を作成することさえできた場合、生のリクエストまたは条件による複数の更新を呼び出すと、すべてが台無しになります。 たとえば、Postgresが変更を追跡し、PGQにすぐに送信するトリガーを作成した場合、...







  2. 競争の激しい環境でカウンターを更新する原子性を観察することもそれほど簡単ではありません。


質問してください。 批判する。 カウンターの扱い方を教えてください。








All Articles