Zähler für Nachrichtenbenachrichtigungen im Django-Framework von Python

高洛峰
Freigeben: 2017-03-03 13:14:11
Original
1173 Leute haben es durchsucht

Der Anfang der Geschichte: .count()

Angenommen, Sie haben eine Benachrichtigungsmodellklasse, die hauptsächlich alle Site-Benachrichtigungen speichert:

class Notification(models.Model):
  """一个简化过的Notification类,拥有三个字段:

  - `user_id`: 消息所有人的用户ID
  - `has_readed`: 表示消息是否已读
  """

  user_id = models.IntegerField(db_index=True)
  has_readed = models.BooleanField(default=False)
Nach dem Login kopieren

Natürlich erhalten Sie zunächst über diese Abfrage die Anzahl der ungelesenen Nachrichten eines bestimmten Benutzers:

# 获取ID为3074的用户的未读消息数
Notification.objects.filter(user_id=3074, has_readed=False).count()
Nach dem Login kopieren

Wenn Ihre Benachrichtigungstabelle relativ klein ist, gibt es mit dieser Methode kein Problem, aber langsam, wenn das Geschäftsvolumen zunimmt. Die Nachrichtentabelle enthält Hunderte Millionen Daten. Viele faule Benutzer haben Tausende ungelesene Nachrichten.

Zu diesem Zeitpunkt müssen Sie einen Zähler implementieren, um die Anzahl der ungelesenen Nachrichten für jeden Benutzer zu zählen. Auf diese Weise müssen wir im Vergleich zum vorherigen count() nur eine einfache Primärschlüsselabfrage durchführen (. oder noch besser) können Sie die Anzahl der ungelesenen Nachrichten in Echtzeit abrufen.

Bessere Lösung: Erstellen Sie einen Zähler
Erstellen wir zunächst eine neue Tabelle, um die Anzahl der ungelesenen Nachrichten für jeden Benutzer zu speichern.

class UserNotificationsCount(models.Model):
  """这个Model保存着每一个用户的未读消息数目"""

  user_id = models.IntegerField(primary_key=True)
  unread_count = models.IntegerField(default=0)

  def __str__(self):
    return &#39;<UserNotificationsCount %s: %s>&#39; % (self.user_id, self.unread_count)
Nach dem Login kopieren

Wir stellen jedem registrierten Benutzer einen entsprechenden UserNotificationsCount-Datensatz zur Verfügung, um seine Anzahl ungelesener Nachrichten zu speichern. Jedes Mal, wenn Sie die Anzahl der ungelesenen Nachrichten erhalten, benötigen Sie nur UserNotificationsCount.objects.get(pk=user_id).unread_count.

Als nächstes kommt hier der Kern der Frage: Woher wissen wir, wann wir unseren Zähler aktualisieren sollten? Bietet Django diesbezüglich irgendwelche Abkürzungen?

Herausforderung: Aktualisieren Sie Ihren Zähler in Echtzeit

Damit unser Zähler ordnungsgemäß funktioniert, müssen wir ihn in Echtzeit aktualisieren, was Folgendes beinhaltet:

  • Wenn eine neue ungelesene Nachricht eingeht, addieren Sie 1 zum Zähler

  • Wenn die Nachricht abnormal gelöscht wird und die zugehörige Nachricht ungelesen ist, setzen Sie sie auf Zähler -1

  • Wenn eine neue Nachricht gelesen wird, -1 für den Zähler

Behandeln wir diese Situationen einzeln.

Bevor wir die Lösung verwerfen, müssen wir eine Funktion in Django einführen: Signals ist ein von Django bereitgestellter Ereignisbenachrichtigungsmechanismus, der es Ihnen ermöglicht, bestimmte benutzerdefinierte oder voreingestellte Ereignisse abzuhören, wenn diese Ereignisse auftreten , werden die durch die Implementierung definierten Methoden aufgerufen.

Zum Beispiel stellen django.db.models.signals.pre_save und django.db.models.signals.post_save Ereignisse dar, die vor und nach dem Aufruf der Speichermethode durch ein Modell ausgelöst werden Von der Datenbank bereitgestellter Trigger. Es gibt einige Ähnlichkeiten in der Funktionalität.

Weitere Informationen zu Signalen finden Sie in der offiziellen Dokumentation. Schauen wir uns an, welche Vorteile Signale für unsere Schalter bieten können.

1. Wenn eine neue Nachricht eingeht, addieren Sie 1 zum Zähler

Diese Situation sollte am besten mit Djangos Signalen bewältigt werden Mit wenigen Codezeilen können wir in diesem Fall die Zähleraktualisierung implementieren:

from django.db.models.signals import post_save, post_delete

def incr_notifications_counter(sender, instance, created, **kwargs):
  # 只有当这个instance是新创建,而且has_readed是默认的false才更新
  if not (created and not instance.has_readed):
    return

  # 调用 update_unread_count 方法来更新计数器 +1
  NotificationController(instance.user_id).update_unread_count(1)

# 监听Notification Model的post_save信号
post_save.connect(incr_notifications_counter, sender=Notification)
Nach dem Login kopieren

Auf diese Weise können Sie jedes Mal, wenn Sie Notification.create oder .save verwenden ( ) und andere Methoden eine neue Benachrichtigung erstellen, wird unser NotificationController benachrichtigt und der Zähler wird +1 sein.

Aber bitte beachten Sie, dass unsere Zähler keine Benachrichtigungen erhalten, wenn Sie irgendwo in Ihrem Code rohes SQL verwenden und keine neuen Benachrichtigungen über die Django-ORM-Methode hinzufügen, da unsere Zähler auf Django-Signalen basieren Am besten standardisieren Sie alle neuen Methoden zur Benachrichtigungserstellung, z. B. durch die Verwendung derselben API.

2. Wenn eine Nachricht abnormal gelöscht wird und die zugehörige Nachricht ungelesen ist, beträgt der Zähler -1

Bei der ersten Erfahrung ist diese Situation auch relativ einfach zu handhaben. Sie müssen nur das post_delete-Signal der Benachrichtigung überwachen. Das Folgende ist ein Beispielcode:

def decr_notifications_counter(sender, instance, **kwargs):
  # 当删除的消息还没有被读过时,计数器 -1
  if not instance.has_readed:
    NotificationController(instance.user_id).update_unread_count(-1)

post_delete.connect(decr_notifications_counter, sender=Notification)
Nach dem Login kopieren


An dieser Stelle: Benachrichtigung Das Löschereignis kann unseren Zähler auch normal aktualisieren.

3. Beim Lesen einer neuen Nachricht beträgt der Zähler -1

Wenn der Benutzer als Nächstes eine ungelesene Nachricht liest, müssen wir auch unseren Zähler für ungelesene Nachrichten aktualisieren . Man könnte sagen: Was ist daran so schwierig? Kann ich meinen Zähler in meiner Methode zum Lesen von Nachrichten einfach manuell aktualisieren?

Zum Beispiel:

class NotificationController(object):

  ... ...

  def mark_as_readed(self, notification_id):
    notification = Notification.objects.get(pk=notification_id)
    # 没有必要重复标记一个已经读过的通知
    if notication.has_readed:
      return

    notification.has_readed = True
    notification.save()
    # 在这里更新我们的计数器,嗯,我感觉好极了
    self.update_unread_count(-1)
Nach dem Login kopieren

Durch einige einfache Tests können Sie feststellen, dass Ihr Zähler sehr gut funktioniert, aber diese Implementierung Die Methode weist ein sehr schwerwiegendes Problem auf. Diese Methode kann gleichzeitige Anforderungen nicht normal verarbeiten.

Zum Beispiel haben Sie ein ungelesenes Nachrichtenobjekt mit der ID 100. Zu diesem Zeitpunkt gehen zwei Anfragen gleichzeitig ein. Sie müssen diese Benachrichtigung als gelesen markieren:

# 因为两个并发的请求,假设这两个方法几乎同时被调用
NotificationController(user_id).mark_as_readed(100)
NotificationController(user_id).mark_as_readed(100)
Nach dem Login kopieren

Offensichtlich markieren beide Methoden diese Benachrichtigung erfolgreich als gelesen, da im Fall der Parallelität Prüfungen wie „notification.has_readed“ nicht ordnungsgemäß funktionieren können, sodass wir den Zähler falsch anzeigen – 1 zweimal, aber tatsächlich haben wir nur eine Anfrage gelesen.

Wie kann man dieses Problem lösen?

Grundsätzlich gibt es nur eine Möglichkeit, durch gleichzeitige Anfragen verursachte Datenkonflikte zu lösen: Es werden zwei relativ einfache Lösungen eingeführt:

使用 select for update 数据库查询

select ... for update 是数据库层面上专门用来解决并发取数据后再修改的场景的,主流的关系数据库 比如mysql、postgresql都支持这个功能, 新版的Django ORM甚至直接提供了这个功能的shortcut 。 关于它的更多介绍,你可以搜索你使用的数据库的介绍文档。

使用 select for update 后,我们的代码可能会变成这样:

from django.db import transaction

class NotificationController(object):

  ... ...

  def mark_as_readed(self, notification_id):
    # 手动让select for update和update语句发生在一个完整的事务里面
    with transaction.commit_on_success():
      # 使用select_for_update来保证并发请求同时只有一个请求在处理,其他的请求
      # 等待锁释放
      notification = Notification.objects.select_for_update().get(pk=notification_id)
      # 没有必要重复标记一个已经读过的通知
      if notication.has_readed:
        return

      notification.has_readed = True
      notification.save()
      # 在这里更新我们的计数器,嗯,我感觉好极了
      self.update_unread_count(-1)
Nach dem Login kopieren

除了使用``select for update``这样的功能,还有一个比较简单的办法来解决这个问题。

使用update来实现原子性修改

其实,更简单的办法,只要把我们的数据库改成单条的update就可以解决并发情况下的问题了:

def mark_as_readed(self, notification_id):
    affected_rows = Notification.objects.filter(pk=notification_id, has_readed=False)\
                      .update(has_readed=True)
    # affected_rows将会返回update语句修改的条目数
    self.update_unread_count(affected_rows)
Nach dem Login kopieren

这样,并发的标记已读操作也可以正确的影响到我们的计数器了。

高性能?
我们在之前介绍了如何实现一个能够正确更新的未读消息计数器,我们可能会直接使用UPDATE 语句来修改我们的计数器,就像这样:

from django.db.models import F

def update_unread_count(self, count)
  # 使用Update语句来更新我们的计数器
  UserNotificationsCount.objects.filter(pk=self.user_id)\
                 .update(unread_count=F(&#39;unread_count&#39;) + count)
Nach dem Login kopieren

但是在生产环境中,这样的处理方式很有可能造成严重的性能问题,因为如果我们的计数器在频繁 更新的话,海量的Update会给数据库造成不小的压力。所以为了实现一个高性能的计数器,我们 需要把改动暂存起来,然后批量写入到数据库。

使用 redis 的 sorted set ,我们可以非常轻松的做到这一点。

使用sorted set来缓存计数器改动

redis是一个非常好用的内存数据库,其中的sorted set是它提供的一种数据类型:有序集合, 使用它,我们可以非常简单的缓存所有的计数器改动,然后批量回写到数据库。

RK_NOTIFICATIONS_COUNTER = &#39;ss_pending_counter_changes&#39;

def update_unread_count(self, count):
  """修改过的update_unread_count方法"""
  redisdb.zincrby(RK_NOTIFICATIONS_COUNTER, str(self.user_id), count)

# 同时我们也需要修改获取用户未读消息数方法,使其获取redis中那些没有被回写
# 到数据库的缓冲区数据。在这里代码就省略了
Nach dem Login kopieren

通过以上的代码,我们把计数器的更新缓冲在了redis里面,我们还需要一个脚本来把这个缓冲区 里面的数据定时回写到数据库中。

通过自定义django的command,我们可以非常轻松的做到这一点:

# File: management/commands/notification_update_counter.py

# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand
from django.db.models import F

# Fix import prob
from notification.models import UserNotificationsCount
from notification.utils import RK_NOTIFICATIONS_COUNTER
from base_redis import redisdb

import logging
logger = logging.getLogger(&#39;stdout&#39;)


class Command(BaseCommand):
  help = &#39;Update UserNotificationsCounter objects, Write changes from redis to database&#39;

  def handle(self, *args, **options):
    # 首先,通过 zrange 命令来获取缓冲区所有修改过的用户ID
    for user_id in redisdb.zrange(RK_NOTIFICATIONS_COUNTER, 0, -1):
      # 这里值得注意,为了保证操作的原子性,我们使用了redisdb的pipeline
      pipe = redisdb.pipeline()
      pipe.zscore(RK_NOTIFICATIONS_COUNTER, user_id)
      pipe.zrem(RK_NOTIFICATIONS_COUNTER, user_id)
      count, _ = pipe.execute()
      count = int(count)
      if not count:
        continue

      logger.info(&#39;Updating unread count user %s: count %s&#39; % (user_id, count))
      UserNotificationsCount.objects.filter(pk=obj.pk)\
                     .update(unread_count=F(&#39;unread_count&#39;) + count)
Nach dem Login kopieren

之后,通过 python manage.py notification_update_counter 这样的命令就可以把缓冲区 里面的改动批量回写到数据库了。我们还可以把这个命令配置到crontab中来定义执行。

总结
文章到了这里,一个简单的“高性能”未读消息计数器算是实现完了。说了这么多,其实主要的知识点就是这么些:

使用Django的signals来获取Model的新建/删除操作更新
使用数据库的select for update来正确处理并发的数据库操作
使用redis的sorted set来缓存计数器的修改操作
希望能对您有所帮助。 :)

更多Python的Django框架中消息通知的计数器相关文章请关注PHP中文网!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage