ホームページ >データベース >Redis >Redisはランキングと同一ポイントを時間順にソートする機能を実装

Redisはランキングと同一ポイントを時間順にソートする機能を実装

WBOY
WBOY転載
2022-08-22 17:51:202337ブラウズ

推奨学習: Redis ビデオ チュートリアル

日々の開発では、ユーザー スコアなどを計算する必要に遭遇することがよくあります。たとえば、ゲームでは戦闘効率をランク付けする必要があり、チーム活動では各チームの貢献値をランク付けする必要があり、WeChat では各友達の歩数をランク付けする必要があります。この場合、一般に、Redis の順序が選択されます。コレクションには、ランキングのニーズを満たすためにユーザーのスコアが保存されます。ただし、シナリオごとのランキング方法も若干異なります。以下は、私の日々の開発に基づいた要約です。

要件: チーム アクティビティにおける各チームの貢献値をランク付けします。

同一のポイントは考慮されません

Redis のソート セットは、文字列型の順序付きセットです。セットのメンバーは一意であるため、セット内に重複したデータが存在することはできません。

各要素は double 型のスコアに関連付けられます。 Redis はスコアを使用して、コレクションのメンバーを小さいものから大きいものまで並べ替えます。

順序付きセットのメンバーは一意ですが、スコアは繰り返すことができます。

同じポイントを持つ状況を無視して、ランキング リストを実装します:

// 准备数据,其中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

Redis のデフォルトの実装では、同じスコアを持つメンバーが辞書順 (09、AZ、上記で使用しているa~z)はzrevrangeなので逆順となり、同じスコアでのソートは時間優先でソートできません。

同じポイントは時間順にソートされ、ランキングは一意になります

上記の実装では、2 つのチームの貢献値が同じ、つまりポイント値が同じである場合、時間に基づいてランク付けすることはできません。

したがって、 スコア = 貢献値のタイムスタンプ を設計する必要があります。スコアが高い人が最初にランクされます。最後に、スコアに基づいて貢献値を分析する必要があります。

設計 1

スコア値の保存には整数を使用します。redis のスコア自体は double 型であり、正確に保存できる最大整数は 2^53=9007199254740992 (16 ビット) です。 )。ミリ秒まで正確なタイムスタンプには 13 桁が必要で、ストレージ寄与値には 3 桁のみが残ります。現在、時刻が秒まで正確な場合、必要なのは 10 桁のみで、寄与値には 6 桁が残ります。

全体的な設計: 上位 3 ビットは寄与値を表し、下位 13 ビットはタイムスタンプを表します。

単純にスコア構造をまとめると、貢献値 * 10^13 タイムスタンプ になります。スコアが大きいほどスコアは近くなり、タイムスタンプが小さいほどスコアは近くなります。このように、2つの判定ルールは逆のものもあり、単純に両者を組み合わせてスコア化することはできません。

しかし、逆に考えて、同じ十分に大きな数値 Integer.MAX からタイムスタンプを減算することもできます。タイムスタンプが小さいほど、得られる差が大きくなるため、スコアの構造を変更できます。値に貢献します。 to: * 10^13 (Integer.MAX-timestamp) これにより、ニーズを満たすことができます。

設計2

redisのスコア値はdouble型なので、整数部分に貢献値を格納し、小数部分にタイムスタンプを格納し、最大値を使用することができます。同じタイムスタンプの部分からそれを減算します。

このようにして、全体的な設計は次のようになります: スコア = 貢献値 (Integer.MAX-タイムスタンプ) * 10^-13

欠点: スコア値は変数を使用して計算されるため、チームに貢献値を追加する場合、以前のzincrbyを使用してスコア値を変更することはできず、同時条件でチームに貢献値を追加すると、スコアが不正確になります。価値観。

エラー状況シミュレーション:

チーム A の現在の貢献値が 10 であるとします。チーム A のプレイヤー X がチームに貢献値を 1 追加し、プログラムでスコアが計算されます。チーム A のプレーヤー Y はチームに貢献値 1 を追加し、スコアはプログラムで 11 になるように計算されます。yyy チーム A のプレーヤー X が redis の zadd コマンドを呼び出して、チームの貢献値を 11 に設定します。 .xxx チーム A のプレイヤー Y が redis の zadd コマンドを呼び出します。 チームの貢献値を 11.yyy に設定します。 最終的に、チーム A の貢献値は 11 と計算されます。貢献値を増やす操作のアトミック性は保証できません。

現時点では、lua スクリプトを使用して、貢献値の計算と設定の 2 つの操作のアトミック性を確保する必要があります。

// 其中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 では time 関数を使用できないため、 (Integer.MAX - タイムスタンプ) * 10^-13 の部分は、スクリプトの外のプログラムによって計算されて渡されます。

ランキング リストのページングやチームのランキングのクエリなどの機能には、引き続き上記のコマンドを使用できます。

同ポイントを時間順に並べた同順位ランキング

いわゆる同順位ランキングとは、順位状況が同じ順位のランキングです。

期待される結果は次のとおりです:

#b992c992##de

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

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视频教程

チーム ID 貢献度 ランキング
a 100 1
88 4
87 5

以上がRedisはランキングと同一ポイントを時間順にソートする機能を実装の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjb51.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。