Python Django 基于 Redis做实时排行榜和排名

本文介绍如何通过使用 redis 的 zset 数据类型实现实时的排行榜功能。
你应该先有 redis-server 服务,你可以自行安装到服务器,也可以购买在线的云 redis 服务。

安装 django-redis

pip install django-redis

配置项目的 settings.py 文件

在 settings.py 文件里面添加 redis 连接的相关配置。

CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://@127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
            "PASSWORD": "*****"
        },
    }
}

对 model 进行处理

在有些业务场景下面,你可能需要通过多个字段进行排名,比如下面这个例子,需要通过用户的段位星星数结合来对用户排名。段位越高排名越靠前,相同段位的情况下,星星数越多越靠前。假设用户的星星数不会超过 99999,通过 100000 * 段位 + 星星数 来计算用于排名的索引值。

import redis
from django.db import models
from django.db.models import F
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django_redis import get_redis_connection


class ParkourLevel(models.Model):
    level_num = models.IntegerField(verbose_name='段位编号', unique=True)
    name = models.CharField(verbose_name='段位名称', max_length=255, unique=True, blank=True)
    stars = models.IntegerField(verbose_name='星星数(升段位)', default=0)

    objects = models.Manager()

    class Meta:
        ordering = ['id']
        verbose_name = '跑酷段位'
        verbose_name_plural = '跑酷段位'

    def __str__(self):
        return '{0}'.format(self.level_num)


class ParkourUser(models.Model):
    user = models.ForeignKey(
        'main.User',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name='用户ID'
    )
    level = models.ForeignKey(
        ParkourLevel,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name='段位'
    )
    stars = models.IntegerField(verbose_name='星星数', default=0)
    # 能量值 = level_num * 100000 + stars,能量值用于排序
    currency = models.IntegerField(verbose_name='能量', default=0)

    objects = models.Manager()

    def calculate_currency(self):
        # 计算能量值,用于排序
        level_num = getattr(self, 'level_num')
        if level_num:
            self.currency = 100000 * int(level_num) + int(self.stars)
        return self.currency

    def save_ranking_data(self):
        # 向 Redis 存跑酷用戶的数据
        self.calculate_currency()
        if self.user_id:
            add_parkour_ranking({int(self.user_id): float(self.currency)})

    @property
    def rank(self):
        """
        :return: 返回用户自己的排名和前n名跑酷用户
        """
        res = {'current_rank': 0, 'tops': []}
        if self.user_id:
            res['current_rank'] = get_current_rank(self.user_id)
        res['tops'] = get_tops(n=99)
        return res

    class Meta:
        ordering = ['-id']
        verbose_name = '跑酷用户'
        verbose_name_plural = '跑酷用户'

    def __str__(self):
        return '用户ID:{0} ,星星:{1} ,能量: {2}'.format(getattr(self, 'user_id'), self.stars, self.currency)

使用 signals 的 pre_save 自动同步数据

通过 signals 的 pre_save 在保持数据的时候,自动将数据同步到 redis 的排行榜。

@receiver(pre_save, sender=ParkourUser)
def pre_save_parkour_user(sender, instance, **kwargs):
    """
    在保存的过程中添加排行榜数据
    """
    instance.save_ranking_data()

使用 redis 的函数添加和获取数据

通过连接池连接 redis

def get_redis():
    """
    获取 Redis 的连接
    """
    pool = get_redis_connection('default').connection_pool
    r = redis.Redis(connection_pool=pool)
    return r

通过 zadd 添加排行榜数据

def add_parkour_ranking(data):
    """
    添加跑酷数据
    """
    if data:
        r = get_redis()
        return r.zadd('parkour', data)            

通过 zrevrank 获取用户的当前排名

def get_current_rank(user_id):
    """
    获取用户的当前排名
    :param user_id: 用户 id
    :return:  返回用户的排名
    """
    r = get_redis()
    rank = r.zrevrank("parkour", user_id)
    if rank is not None:
        rank = rank + 1
    else:
        rank = 0
    return rank

通过 zrevrange 获取指定区间的用户

def get_tops(n=99):
    """
    获取前n名用户的信息
    :param n: 排行榜的长度
    """

    r = get_redis()
    tops = r.zrevrange("parkour", 0, n, withscores=True)
    user_ids = list()
    for t in tops:
        try:
            user_ids.append(int(t[0].decode()))
        except (ValueError, TypeError):
            pass
    top_users = ParkourUser.objects.filter(
        user_id__in=user_ids
    ).annotate(
        level_num=F('level__level_num'),
        baby_name=F('user__baby_name'),
    ).order_by(
        '-currency'
    ).values(
        'user_id', 'baby_name', 'level_num', 'stars', 'user__user_icon_image'
    )

    # 将列表转为字典
    tops_dict = dict()
    for t in top_users:
        tops_dict[t['user_id']] = t

    res_tops = list()
    for user_id in user_ids:
        try:
            res_tops.append(tops_dict[user_id])
        except KeyError:
            pass
    return res_tops
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值