目录
本文介绍如何通过使用 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