Maison  >  Article  >  base de données  >  Exemples détaillés de la façon dont Redis implémente les classements et la même fonction de tri des points par temps

Exemples détaillés de la façon dont Redis implémente les classements et la même fonction de tri des points par temps

WBOY
WBOYavant
2022-08-26 14:09:491748parcourir

Apprentissage recommandé : Tutoriel vidéo Redis

Dans le développement quotidien, nous rencontrons souvent le besoin de trier les scores des utilisateurs, etc., comme le classement de la puissance de combat dans les jeux et dans les activités d'équipe. Il est nécessaire de classer la valeur de la contribution de chaque équipe.Dans WeChat, il est nécessaire de classer le nombre de pas de chaque ami. À ce stade, la collection ordonnée de redis est généralement sélectionnée pour stocker le score de l'utilisateur afin de répondre aux besoins du classement, mais différent La méthode de classement des scènes est également légèrement différente. Ce qui suit est un résumé basé sur mon développement quotidien.

Exigence : Classez la valeur de la contribution de chaque équipe dans l'activité d'équipe.

Ne considérez pas que l'intégrale est la même

L'ensemble trié de Redis est un ensemble ordonné de type String. Les membres de l'ensemble sont uniques, ce qui signifie que les données en double ne peuvent pas apparaître dans l'ensemble.

Chaque élément est associé à une partition de type double. Redis utilise des scores pour trier les membres de la collection du plus petit au plus grand.

Les membres d'un ensemble ordonné sont uniques, mais les scores peuvent être répétés.

Ignorez le cas des mêmes points et implémentez la liste de classement :

// 准备数据,其中value为每个队伍的ID,score为队伍的贡献值
> zadd z1 5 a 6 b 1 c 2 d 10 e
(integer) 5

// 分页查询排行榜所有的队伍和贡献值,要使用zrevrange,而不是zrange,贡献值越大越排在前面
> zrevrange z1 0 2 withscores
1) "e"
2) "10"
3) "b"
4) "6"
5) "a"
6) "5"

// 增加某个队伍的贡献值
> zincrby z1 3 d
"5"
> zincrby z1 4 c
"5"

// 查询排行榜所有的队伍
> zrevrange z1 0 -1 withscores
 1) "e"
 2) "10"
 3) "b"
 4) "6"
 5) "d"
 6) "5"
 7) "c"
 8) "5"
 9) "a"
10) "5"

// 查询某个队伍的排名
> zrevrank z1 d
(integer) 2

L'implémentation par défaut de Redis consiste à trier les membres avec le même score dans l'ordre du dictionnaire (09, AZ, a~z). c'est donc dans l'ordre inverse. Par conséquent, le tri avec le même score ne peut pas être trié en fonction de la priorité temporelle.

Les mêmes points sont triés par temps et le classement est unique

Dans la mise en œuvre ci-dessus, si la valeur de contribution de deux équipes est la même, c'est-à-dire que la valeur des points est la même, elles ne peuvent pas être classées en fonction du temps.

Nous devons donc concevoir un score = valeur de contribution + horodatage Celui qui a un score plus élevé sera classé en premier. Enfin, la valeur de contribution doit être analysée en fonction du score.

Conception 1

Utilisez des entiers pour stocker les valeurs de score. Le score lui-même dans Redis est de type double, et le nombre entier maximum pouvant être stocké avec précision est 2^53=9007199254740992 (16 bits). Un horodatage précis en millisecondes nécessite 13 chiffres, ne laissant que 3 chiffres pour la valeur de contribution au stockage. Actuellement, si l'heure est précise en secondes, seuls 10 chiffres sont nécessaires, ce qui laisse 6 chiffres pour la valeur de contribution.

Conception générale : les 3 chiffres supérieurs représentent la valeur de la contribution et les 13 chiffres inférieurs représentent l'horodatage.

Si on rassemble simplement la structure du score : Valeur de la contribution * 10^13 + horodatage, car plus le score est grand, plus il sera proche, et plus l'horodatage est petit, plus il sera proche . De cette façon, les deux parties Les règles de jugement sont opposées et les deux ne peuvent pas être simplement combinées en une partition. 贡献值 * 10^13 + 时间戳 拼凑,因为分数越大越靠前,而时间戳越小则越靠前,这样两部分的判断规则是相反的,无法简单把两者合成一起成为score。

但是我们可以逆向思维,可以用同一个足够大的数Integer.MAX减去时间戳,时间戳越小,则得到的差值越大,这样我们就可以把score的结构改为:贡献值 * 10^13 + (Integer.MAX-时间戳),这样就能满足我们的需求了。

设计2

由于redis的score值是double类型,可以使用整数部分存储贡献值,小数部分存储时间戳,同样时间戳的部分使用一个最大值减去它。

这样,整体设计变为:分数=贡献值 + (Integer.MAX-时间戳) * 10^-13

弊端:由于分数值是由两个变量来计算得出,所以在给队伍增加贡献值时,无法简单的使用之前的zincrby来改变score的值了,这样在并发情况下为队伍增加贡献值就会导致score值不准确。

错误情况模拟:

假设现在队伍A的贡献值为10队伍A中的队员X为队伍增加贡献值1,在程序中算出score为11.xxx队伍A中的队员Y为队伍增加贡献值1,在程序中算出score为11.yyy队伍A中的队员X调用redis的zadd命令设置队伍的贡献值为11.xxx队伍A中的队员Y调用redis的zadd命令设置队伍的贡献值为11.yyy最后算出队伍A的贡献值为11,无法保证增加贡献值这一个操作的原子性。

此时需要借助lua脚本来保证计算和设置贡献值这两个操作的原子性:

// 其中KEYS[1]为排行榜key,KEYS[2]为队伍ID
// 其中ARGV[1]为增加的贡献值,ARGV[2]为Integer.MAX-时间戳
local score = redis.call('zscore', KEYS[1], KEYS[2]) 
if not(score) then
	score=0 
end 
score=math.floor(score) + tonumber(ARGV[1]) + tonumber(ARGV[2]) 
redis.call('zadd', KEYS[1], score, KEYS[2]) return 1

由于redis中无法使用时间函数,所以(Integer.MAX-时间戳) * 10^-13

Mais nous pouvons penser à l'envers et soustraire l'horodatage du même nombre assez grand Integer.MAX. Plus l'horodatage est petit, plus la différence est grande, nous pouvons donc changer la structure du score en : Valeur de la contribution * 10. ^13 + (Integer.MAX-timestamp), cela peut répondre à nos besoins.

Design 2

Étant donné que la valeur du score de redis est de type double, vous pouvez utiliser la partie entière pour stocker la valeur de contribution, la partie décimale pour stocker l'horodatage et utiliser une valeur maximale pour la soustraire de la partie du même horodatage.

De cette façon, la conception globale devient : Score = valeur de contribution + (Integer.MAX-timestamp) * 10^-13

Inconvénients : Puisque la valeur du score est calculée par deux variables, donc lorsque vous ajoutez une valeur de contribution à l'équipe, vous ne pouvez pas simplement utiliser le zincrby précédent pour modifier la valeur du score. De cette manière, l'ajout d'une valeur de contribution à l'équipe dans des conditions concurrentes entraînera des valeurs de score inexactes. Simulation de situation d'erreur :
> zscore 排行榜key teamId
> zrevrangebyscore(排行榜key, 上一步得到的score+1, 上一步得到的score, limit, 0 , 1)
> zrevrank(排行榜key, 上一步得到的teamId)
Vous pouvez continuer à utiliser les commandes ci-dessus pour interroger les classements dans les pages, interroger les classements des équipes et d'autres fonctions. ID de l'équipeValeur de la contributionClassementa1001b99 2 c 992
Supposons que la valeur de contribution actuelle de l'équipe A soit de 10. Le joueur X de l'équipe A ajoute 1 valeur de contribution à l'équipe et le score est calculé dans le programme pour être de 11.xxx Joueur Y de l'équipe A ajoute une valeur de contribution à l'équipe 1. Calculez le score dans le programme à 11.yyy. Le joueur yyy a finalement calculé la valeur de contribution de l'équipe A à 11, et l'atomicité de l'opération d'augmentation de la valeur de contribution ne peut pas être garantie. À ce stade, vous devez utiliser un script Lua pour assurer l'atomicité des deux opérations de calcul et de définition de la valeur de contribution : Étant donné que la fonction time ne peut pas être utilisée dans redis, (Integer.MAX- timestamp) * 10^- La partie 13 est calculée et transmise par le programme en dehors du script.
Ceux avec les mêmes points sont triés par temps et classés en parallèle Les classements dits ex aequo sont des classements avec la même situation de classement. Les résultats que nous attendons sont les suivants :
🎜d🎜🎜88🎜🎜4🎜🎜🎜🎜e🎜🎜87🎜🎜5🎜🎜🎜🎜

当然现实中也有排名不跳过的情况,我这里考虑的是排名跳过的情况。

redis中score的设计还是采用上面的分数=贡献值 + (Integer.MAX-时间戳) * 10^-13,只是在查询排名时需要进行计算。

比如要查上表中队伍b的排名,思路如下:

  • 首先查到队伍b的score
  • 再查到跟队伍b的score的整数部分相同(也就是贡献值一样),排在第一个的队伍的value(队伍ID)
  • 根据上一步得到的队伍ID查询此队伍的排名就是队伍b的排名

使用命令实现上面的步骤如下:

> zscore 排行榜key teamId
> zrevrangebyscore(排行榜key, 上一步得到的score+1, 上一步得到的score, limit, 0 , 1)
> zrevrank(排行榜key, 上一步得到的teamId)

为了性能考虑,可以使用下面的脚本一次查出来:

// KEYS[1]表示排行榜key
// KEYS[2]表示要查询的队伍的ID
local rank = 0 
local score = redis.call('zscore', KEYS[1], KEYS[2]) 
if not(score) then
    score=0 
else 
    score=math.floor(score) 
    local firstScore = redis.call('zrevrangebyscore', KEYS[1], score+1, score, 'limit', 0, 1) 
    rank=redis.call('zrevrank', KEYS[1], firstScore[1]) 
end 
return {score,rank}

下面附上分页查询排行榜的脚本,假如一页10条,不用下面的脚本需要查询10次上面的脚本,如果连上面的脚本都没有使用的话就要查询30次redis。

// 排行榜key
// ARGV[1]分页起始偏移
// ARGV[2]分页结束偏移
local list = redis.call('zrevrange', KEYS[1], ARGV[1], ARGV[2], 'withscores') 
local result={} 
local i = 1 
for k,v in pairs(list) do 
    if k%2 == 0 then 
        local teamId = list[k-1] 
        local score = math.floor(v) 
        local firstScore = redis.call('zrevrangebyscore', KEYS[1], score+1, score, 'limit', 0, 1) 
        local rank=redis.call('zrevrank', KEYS[1], firstScore[1]) 
        local l = {teamId=teamId, contributionValue=score, teamRank=rank+1} 
        result[i] = l i = i + 1 
    end 
end 
return cjson.encode(result)

此脚本使用了cjson库,返回的是一个json。

推荐学习:Redis视频教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer