Django의 고성능 카운터(Counter) 인스턴스

10030 단어
카운터(Counter)는 매우 자주 사용하는 기능 구성 요소로 이 블로그는 읽지 않은 메시지 수를 예로 들어 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 '' % (self.user_id, self.unread_count)


우리는 모든 등록 사용자에게 읽지 않은 메시지 수를 저장하기 위한 UserNotifications Count 기록을 제공합니다.읽지 않은 메시지 수를 얻을 때마다 UserNotifications Count만 필요합니다.objects.get(pk=user_id).unread_카운트만 하면 돼요.
다음에 문제의 중점이 왔다. 우리는 언제 우리의 계수기를 업데이트해야 하는지 어떻게 알 수 있습니까?Django가 이 방면에 어떤 지름길을 제공했습니까?
도전: 카운터 실시간 업데이트
카운터가 제대로 작동하려면 다음과 같이 실시간으로 업데이트해야 합니다.
1. 새로운 미읽기 메시지가 왔을 때 카운터 +12.메시지가 비정상적으로 삭제되었을 때 관련 메시지가 읽지 않은 경우 카운터-13.새로운 소식을 다 읽었을 때 카운터-1을 위해 하나씩 해결해 봅시다.
해결 방안을 내놓기 전에 우리는 먼저 Django의 기능을 소개해야 한다. Signals, Signals는django가 제공하는 이벤트 알림 메커니즘이다. 사용자 정의나 미리 설정된 이벤트를 감청할 수 있고, 이 이벤트가 발생할 때 정의된 방법을 호출할 수 있다.
예컨대django.db.models.signals.pre_save & django.db.models.signals.post_save는 어떤 모델이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 () 와 같은 방법으로 새 알림을 만들 때, 우리 Notification Controller는 카운터 +1으로 알림을 받을 수 있습니다.
그러나 우리의 계수기는 Django의signals를 기반으로 하기 때문에 만약에 코드에 원시 ql를 사용하는 곳이 있다면 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 같은 검사는 정상적으로 작동하지 않기 때문에 계수기가 잘못될 수 있습니다. - 한두 번, 사실 요청 하나만 읽었습니다.
그렇다면 이런 문제는 어떻게 해결해야 하나요?
기본적으로 동시 요청으로 인한 데이터 충돌을 해결하는 방법은 단 한 가지뿐이다. 잠금을 풀고 두 가지 비교적 간단한 해결 방안을 소개한다.
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``와 같은 기능을 사용하는 것 외에 비교적 간단한 방법으로 이 문제를 해결할 수 있다.
업데이트를 사용하여 원자성 수정
사실, 더욱 간단한 방법은 우리의 데이터베이스를 하나의 업데이트로 바꾸기만 하면 병발 상황의 문제를 해결할 수 있다.
 
  
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('unread_count') + count)


그러나 생산 환경에서 이러한 처리 방식은 심각한 성능 문제를 초래할 수 있다. 왜냐하면 우리의 계수기가 빈번하게 업데이트되면 대량의 Update가 데이터베이스에 적지 않은 압력을 줄 수 있기 때문이다.그래서 고성능의 계수기를 실현하기 위해서 우리는 변경 사항을 잠시 저장한 후에 대량으로 데이터베이스에 기록해야 한다.
Redis의sorted set을 사용하면 우리는 이 점을 매우 쉽게 할 수 있다.
sorted set을 사용하여 캐시 카운터 변경
redis는 매우 유용한 메모리 데이터베이스입니다. 그 중에서sorted set은 그것이 제공하는 데이터 형식입니다. 질서정연한 집합입니다. 이 데이터베이스를 사용하면 우리는 모든 계수기를 간단하게 캐시해서 데이터베이스에 대량으로 쓸 수 있습니다.
 
  
RK_NOTIFICATIONS_COUNTER = 'ss_pending_counter_changes'

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('stdout')


class Command(BaseCommand):
    help = 'Update UserNotificationsCounter objects, Write changes from redis to database'

    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('Updating unread count user %s: count %s' % (user_id, count))
            UserNotificationsCount.objects.filter(pk=obj.pk)\
                                          .update(unread_count=F('unread_count') + count)


이후python 관리자를 통해py notification_update_counter 같은 명령은 버퍼 안의 변경 사항을 대량으로 데이터베이스에 기록할 수 있습니다.이 명령을crontab에 설정해서 실행을 정의할 수도 있습니다.
총결산
글이 여기까지 왔는데, 간단한 '고성능' 미독 메시지 카운터가 완성된 셈이다.이렇게 많이 말했는데, 사실 주요한 지식점은 바로 이렇다.
1. Django의 signals를 사용하여 Model의 새로 만들기/삭제 작업 업데이트를 가져옵니다. 2.데이터베이스의 select for update를 사용하여 병발된 데이터베이스 조작을 정확하게 처리합니다.redis의sorted set을 사용하여 캐시 카운터의 수정 작업이 도움이 되었으면 합니다.:)

좋은 웹페이지 즐겨찾기