Redis 랭킹 동점 처리
알고 있는 레디스 랭킹 동점 처리 관련 기법(?)을 몇 개 정리해보았다. (JS)
1. 기본
redisClient.ZREVRANGE("ranking:myRank", 0, -1, 'WITHSCORES', PrintRedisRank);
0) E : 100
1) D : 100
2) A : 100
3) B : 95
4) C : 70
기본적으로 Redis Rank(ZSet)은 score를 기준으로 정렬한 다음에 동점자가 있으면 key를 기준으로 다시 정렬해서 준다.
이외의 동점자 기준은 Redis에서 지원해주지 않아서 서버 애플리케이션 단에서 처리해줘야 한다.
2. 동점자는 같은 등수로
async function PrintRank( name ){
// 먼저 랭킹을 가져올 유저의 score를 가져온다.
var score = await ZSCORE("ranking:myRank", name);
// 해당 스코어의 랭킹 리스트를 들고온다.
var sameScorelist = await ZREVRANGEBYSCORE("ranking:myRank", score, score);
// 랭킹 리스트의 맨 처음 Key의 랭킹을 가져온다.
var firstRankKey = sameScorelist[0];
var rank = await ZREVRANK("ranking:myRank", firstRankKey);
console.log( name + " : " + rank);
}
PrintRank( 'A' );
PrintRank( 'B' );
PrintRank( 'C' );
PrintRank( 'D' );
PrintRank( 'E' );
A : 0
B : 3
C : 4
D : 0
E : 0
먼저 해당 Key의 점수를 가져오고(ZSCORE),
그 점수의 Rank 리스트를 가져오고(ZREVRANGEBYSCORE),
그 리스트의 첫 Key의 랭킹을 가져오면 된다.(ZREVRANK)
Range로 가져올 때는 같은 점수는 맨 위 랭킹부터 직접 루프를 돌며 고쳐주면 된다.
3. 멀티 스코어 랭킹
점수가 같을 때, 2차 기준을 고려해보는 방법도 있다.
3-1. 두 개의 Score를 사용하기
async function IsNeedToSwap( row1, row2 ){
var result1 = await ZREVRANK("ranking:myRank2", row1.key);
var result2 = await ZREVRANK("ranking:myRank2", row2.key);
return result1 > result2;
}
var result = await ZREVRANGE("ranking:myRank", 0, -1, 'WITHSCORES');
var row = ToRow(result);
for(var i=0; i<row.length; ++i){
for(var j=i+1; j<row.length; ++j){
if(row[i].score != row[j].score){
break;
}
if( await IsResortable( row[i], row[j] ) == true ){
row.swap(i, j);
}
}
}
PrintRow(row);
{ key: 'A', score: '100' }
{ key: 'D', score: '100' }
{ key: 'E', score: '100' }
{ key: 'B', score: '95' }
{ key: 'C', score: '70' }
myRank (1차 기준)
0) E : 100
1) D : 100
2) A : 100
3) B : 95
4) C : 70
myRank2 (2차 기준)
0) A : 100
1) B : 95
2) C : 70
3) D : 44
4) E : 21
단순하고 문제가 크게 없지만, 메모리를 2배로 써야 하는 방법이다.
그냥 랭킹을 2개를 메기고, 1차 기준으로 랭킹을 뽑은 뒤, 동점자는 2차 기준으로 다시 랭킹을 메기는 것이다.
여기서도 동점자가 발생한다면..... [2. 동점자는 같은 등수로]를 같이 쓰면 될 듯하다.
(이 방법으로 TOP 10을 정하려면, 10명 이상이 동점자 일 경우를 대비해서, 1등의 Score와 같은 유저를 긁고 2차 기준으로 재정렬.. 그다음 등수의 Score를 긁고 2차기준으로 재정렬... 을 하면서 10명을 채워야한다. ㅠㅠ)
주변 랭킹, 탑 랭킹 정할때 진짜 쓰레기 같이 짜야하는데 이 방법으로 짤빠에 Redis를 포기하는게 낫다.
3-2 하나의 스코어에 두 개의 값을 메기기
function ZADD( key, value1, value2 ){
var redisScore = Number(value1) + ( Number(value2) * 0.0000001 );
redisClient.ZADD("ranking:myRank", redisScore, key );
}
ZADD( 'A', 100, 100 );
ZADD( 'B', 95, 95 );
ZADD( 'C', 70, 70 );
ZADD( 'D', 100, 44 );
ZADD( 'E', 100, 21 );
redisClient.ZREVRANGE("ranking:myRank", 0, -1, 'WITHSCORES', PrintRedisRank);
0) A : 100.000009999999
1) D : 100.0000044
2) E : 100.000002100001
3) B : 95.0000095000004
4) C : 70.0000069999999
Redis ZSet은 double을 사용한다.
그래서 소수점을 이용해서 위와 같은 꼼수를 사용할 수 있다.
2차 기준이 그렇게 중요하지 않고, 두 score를 낮은 숫자로 제한할 수 있다면, 충분히 고려할만한 것 같다.
부동소수점이 걱정 된다면, value1*10000 + value2와 같은 방법으로 할 수도 있다.