搜尋
首頁後端開發Python教學Python的Django框架中訊息通知的計數器

Python的Django框架中訊息通知的計數器

Mar 03, 2017 pm 01:14 PM
django計數器

故事的開始:.count()

假設你有一個Notification Model類,保存的主要是所有的站內通知:

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

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

  user_id = models.IntegerField(db_index=True)
  has_readed = models.BooleanField(default=False)

理所當然的,剛開始你會透過這樣的查詢來取得某個使用者的未讀訊息數:

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

當你的Notification表比較小的時候,這樣的方式沒有任何的問題,但是慢慢的,隨著業務量的擴大。訊息表裡面有了 上億條資料 。很多懶惰的用戶的未讀訊息數都到了上千條。

這時候,你就需要實作一個計數器,讓這個計數器來統計每個使用者的未讀訊息數,這樣比起之前的count() ,我們只需要執行一個簡單的主鍵查詢(或者更優)就可以拿到即時的未讀訊息數了。

更優的方案:建立計數器
首先,讓我們得建立一個新表來儲存每個使用者的未讀訊息數。

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)

我們為每個註冊使用者提供了一個對應的 UserNotificationsCount 記錄來保存他的未讀訊息數。 每次取得他的未讀取訊息數的時候,只需要 UserNotificationsCount.objects.get(pk=user_id).unread_count 就可以了。

接下來,問題的重點來了,我們如何知道什麼時候應該更新我們的計數器? Django在這方面提供了什麼捷徑嗎?

挑戰:即時更新你的計數器

為了讓我們的計數器正常的工作,我們必須即時的更新它,這包括:

  • 當有新的未讀訊息過來的時候,為計數器+1

  • 當訊息被異常刪除時,如果關聯的訊息為未讀,為計數器-1

  • 當閱讀完一個新訊息​​的時候,為計數器-1

讓我們一個一個來解決這些情況。

在拋出解決方案之前,我們需要先介紹Django中的一個功能: Signals ,Signals是django提供的一個事件通知機制,它可以讓你在監聽某些自訂或預設的事件,當這些事件發生的時候,呼叫實作定義好的方法。

例如django.db.models.signals.pre_save & django.db.models.signals.post_save 表示的是某個Model呼叫save方法之前和之後會觸發的事件,它和Database提供的觸發器在功能上有一點相似。

關於Signals更多的介紹可以參考官方文檔,以下讓我們來看看Signals能為我們的計數器帶來什麼好處。

1. 當有新的訊息過來的時候,為計數器+1

這個情況應該是最好處理的,使用Django的Signals,只需要短短幾行程式碼,我們便可以實現這種情況下的計數器更新:

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)

這樣,每當你使用Notification.create 或.save()之類的方法建立新通知時,我們的NotificationController 就會得到通知,為計數器+1。

但請注意,因為我們的計數器是基於Django的signals,如果你的程式碼裡面有地方在使用原始sql,沒有透過Django ORM方法來新增通知的話,我們的計數器是不會得到通知的,所以,最好規範所有的新通知建立方式,例如使用同一個API。

2. 當訊息被異常刪除時,如果關聯的訊息為未讀,為計數器-1

有了第一個的經驗,這種情況處理起來也比較簡單,只需要監控Notification的post_delete 訊號就可以了,以下是一段實例程式碼:

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)


至此,Notification的刪除事件也能正常的更新我們的計數器了。

3. 當閱讀一個新訊息​​的時候,為計數器-1

接下來,當使用者閱讀某條未讀訊息的時候,我們也需要更新我們的未讀訊息計數器。 你可能會說,這有什麼難的?我只要在我的閱讀訊息的方法裡面,手動更新我的計數器不就好了?

例如這樣:

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)

透過一些簡單的測試,你可以會覺得你的計數器工作的非常好,但是,這樣的實作方式有一個非常致命的問題, 這個方式沒有辦法正常處理並發的請求。

打一個比方,你擁有一個id為100的未讀訊息對象,這個時候同時有了兩個請求過來,都要標記這個通知為已讀:

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

顯而易見的,這兩個方法都會成功的標記這條通知為已讀,因為在並發的情況下, if notification.has_readed 這樣的檢查無法正常工作,所以我們的計數器將會被錯誤的-1 兩次,但其實我們只讀了一條請求。

那麼,這樣的問題該怎麼解決呢?

基本上,解決並發請求產生的資料衝突只有一個方法: 加鎖 ,介紹兩種比較簡單的解決方案:

使用 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)

除了使用``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)

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

高性能?
我们在之前介绍了如何实现一个能够正确更新的未读消息计数器,我们可能会直接使用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)

但是在生产环境中,这样的处理方式很有可能造成严重的性能问题,因为如果我们的计数器在频繁 更新的话,海量的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中那些没有被回写
# 到数据库的缓冲区数据。在这里代码就省略了

通过以上的代码,我们把计数器的更新缓冲在了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)

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

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

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

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

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
Python的科學計算中如何使用陣列?Python的科學計算中如何使用陣列?Apr 25, 2025 am 12:28 AM

Arraysinpython,尤其是Vianumpy,ArecrucialInsCientificComputingfortheireftheireffertheireffertheirefferthe.1)Heasuedfornumerericalicerationalation,dataAnalysis和Machinelearning.2)Numpy'Simpy'Simpy'simplementIncressionSressirestrionsfasteroperoperoperationspasterationspasterationspasterationspasterationspasterationsthanpythonlists.3)inthanypythonlists.3)andAreseNableAblequick

您如何處理同一系統上的不同Python版本?您如何處理同一系統上的不同Python版本?Apr 25, 2025 am 12:24 AM

你可以通過使用pyenv、venv和Anaconda來管理不同的Python版本。 1)使用pyenv管理多個Python版本:安裝pyenv,設置全局和本地版本。 2)使用venv創建虛擬環境以隔離項目依賴。 3)使用Anaconda管理數據科學項目中的Python版本。 4)保留系統Python用於系統級任務。通過這些工具和策略,你可以有效地管理不同版本的Python,確保項目順利運行。

與標準Python陣列相比,使用Numpy數組的一些優點是什麼?與標準Python陣列相比,使用Numpy數組的一些優點是什麼?Apr 25, 2025 am 12:21 AM

numpyarrayshaveseveraladagesoverandastardandpythonarrays:1)基於基於duetoc的iMplation,2)2)他們的aremoremoremorymorymoremorymoremorymoremorymoremoremory,尤其是WithlargedAtasets和3)效率化,效率化,矢量化函數函數函數函數構成和穩定性構成和穩定性的操作,製造

陣列的同質性質如何影響性能?陣列的同質性質如何影響性能?Apr 25, 2025 am 12:13 AM

數組的同質性對性能的影響是雙重的:1)同質性允許編譯器優化內存訪問,提高性能;2)但限制了類型多樣性,可能導致效率低下。總之,選擇合適的數據結構至關重要。

編寫可執行python腳本的最佳實踐是什麼?編寫可執行python腳本的最佳實踐是什麼?Apr 25, 2025 am 12:11 AM

到CraftCraftExecutablePythcripts,lollow TheSebestPractices:1)Addashebangline(#!/usr/usr/bin/envpython3)tomakethescriptexecutable.2)setpermissionswithchmodwithchmod xyour_script.3)

Numpy數組與使用數組模塊創建的數組有何不同?Numpy數組與使用數組模塊創建的數組有何不同?Apr 24, 2025 pm 03:53 PM

numpyArraysareAreBetterFornumericalialoperations andmulti-demensionaldata,而learthearrayModuleSutableforbasic,內存效率段

Numpy數組的使用與使用Python中的數組模塊陣列相比如何?Numpy數組的使用與使用Python中的數組模塊陣列相比如何?Apr 24, 2025 pm 03:49 PM

numpyArraySareAreBetterForHeAvyNumericalComputing,而lelethearRayModulesiutable-usemoblemory-connerage-inderabledsswithSimpleDatateTypes.1)NumpyArsofferVerverVerverVerverVersAtility andPerformanceForlargedForlargedAtatasetSetsAtsAndAtasEndCompleXoper.2)

CTYPES模塊與Python中的數組有何關係?CTYPES模塊與Python中的數組有何關係?Apr 24, 2025 pm 03:45 PM

ctypesallowscreatingingangandmanipulatingc-stylarraysinpython.1)usectypestoInterfacewithClibrariesForperfermance.2)createc-stylec-stylec-stylarraysfornumericalcomputations.3)passarraystocfunctions foreforfunctionsforeffortions.however.however,However,HoweverofiousofmemoryManageManiverage,Pressiveo,Pressivero

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具