使用Redis实现积分排行榜,并支持同积分按时间排序

Posted 仲翎逸仙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Redis实现积分排行榜,并支持同积分按时间排序相关的知识,希望对你有一定的参考价值。

排行榜这个功能很常见,多用于激励用户活跃和拉新,比如CSDN平台实现的周榜,按照每周文章总阅读量进行排名,用排名和奖品激励用户持续在平台上输出高质量内容。

最近笔者也做了一个积分排行榜的功能,在某些场景下我们需要处理同分排名问题。

如张三和李四、王五等人的积分都是100,我们需要实现按最先达到100积分的顺序对他们进行排名,也就是需要按时间排名。

使用Redis实现实时更新的排行榜并不难,Redis提供的ZSet数据结构就很适合用于实现排行榜,但如何实现相同积分情况下再支持按时间排序呢?
实现思路

可能是在此之前笔者刚实现分布式ID生成Base Service,下意识想到了分布式ID雪花算法的原理,即用一个long类型变量存储多个信息。一个long类型长度为8个字节(64bit),雪花算法使用其中41bit记录时间戳,其余bit位存储机房id、机器id、序列号。

Redis的ZSet支持分值为double类型,也是8字节,那么我们也可以使用41位存储时间戳,其实位存储用户的实际积分。

在雪花算法中最高位是不用的,目的是不允许生成负数ID,而在实现排行榜中没有这个限制,因为我们最终要的只是用户的积分,而不是加上时间戳的分值。但也要求最高位要么全为0,要么全为1,避免排序错乱。如实现积分倒序排名时可设置最高位全为1,只不过ZSet已经支持倒序获取,不需要多此一举,所以最高位我们依然不使用。

除去最高位和存储时间戳的41位后,剩余22位表示积分,这时我们还需要结合业务考虑,如果觉得22bit不够表示积分,那么还可以继续压缩时间戳占用的bit。

由于排行榜是周期性的,如周榜、月榜,所以我们没必要存储完整的时间戳,可以取当前时间与周期开始时间相差的毫秒数,这样就可以将41bit压缩到32bit、16bit、或者更低,具体需要多少个bit留给看官们自己算啦。

如果是用41bit表示时间戳,22bit表示积分的话,那么score的组成就是这样的:
0(最高位不用)|0000000 00000000 0000000(22bit表示积分)|0
00000000 00000000 00000000 00000000 00000000(41bit表示时间戳)

因为排序首先按积分排再按时间排,所以积分在高位,时间戳在低位,这样不管时间戳的值是多少,积分越大,64bit表示的数值就越大。

不过这样还没完。

在积分相同的情况下,是不是时间戳越大64bit表示的数值就越大?而我们需要的是按时间升序排,也就是最先达到xx积分的用户排在最前面,所以我们不能单纯的使用41bit存储时间戳,而应该是存储一个随时间流逝而变小的数值。

由于排行榜都会有一个周期,如周榜是一周,月榜是一个月,所以我们使用41bit存储的是一个周期的结束时间yyy-MM-dd 23:59:59对应的时间戳与用户积分更新时间的时间戳的差值,这个值会随着时间的推移而变小,而且不会出现负数的情况,刚好能够达到目的。
实现关键代码

1.实现积分+时间戳差值转score

// periodEndTimestamp: 当前周期结束时间的时间戳 
// 需确保point不会超过22bit所能表示的数值:2097151
private static long toScore(int point, long periodEndTimestamp) 
    long score = 0L;
    score = (score | point) << 41;
    score = score | (periodEndTimestamp - TimestampUtils.currentTimeMillis());
    return score;

2.实现从score中获取积分

private static int getPoint(long score) 
     return (int) (score >> 41);

3.更新积分

@Override
public void updateRanking(Integer periodId, Long accountId, Integer addPoint) 
    String key = String.format(RankingCacheKeys.REALTIME_POINT_RANKING_KEY, periodId);
    Double score = redisTemplate.opsForZSet().score(key, String.valueOf(accountId));
    score = (score == null) ? 0d : score;
    int curPoint = getPoint(score.longValue());
    long newScore = toScore(curPoint + addPoint, getCurPeriodEndDateTimestamp(periodId));
    redisTemplate.opsForZSet().add(key, String.valueOf(accountId), newScore);

总结

基于Redis ZSet实现积分排行榜(倒序)并支持按时间(升序)排序原理与注意事项:

1.将分值score的8字节拆分使用,最高位不用,其余一部分存储实际分值,一部分存储时间戳;

2.先按积分排序,再按时间排序,所以需要高位存储积分,低位存储时间戳,这样才能保证积分越高对应score越大;

3.同分值情况下按时间升序排序,必然让达到当前积分时间最早的score越大;

4.避免积分或者时间戳溢出,如8bit最大可以表示255,如果积分最大可以超过255,那么就需要考虑给积分加到9位…

5.由于每次更新用户积分都需要重新计算score,不能使用ZSet的原子性操作命令,因此可能存在并发数据一致性问题,这点需要考虑。

留一个思考题:如果再加一个条件排序,你觉得能实现吗?
关于实现排行榜用到ZSet的几个命令

1.获取倒序排名:

ZREVRANGEBYSCORE key min max offset count

2.获取某个用户的score:

ZSCORE key member

3.获取参与排名的总用户数(本期参与人数):

ZCARD key

4.获取某个用户的当前排名(倒序排序,从大到小的排名):

ZREVRANK key member
————————————————
版权声明:本文为CSDN博主「Java艺术」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/baidu_28523317/article/details/117829202

以上是关于使用Redis实现积分排行榜,并支持同积分按时间排序的主要内容,如果未能解决你的问题,请参考以下文章

使用Redis实现积分排行榜,并支持同积分按时间排序

redis实现积分排行榜

redis实现积分排行榜

Review代码思考:排行榜同积分按时间排序优化方案 | Lua开发实战

使用Redis实现用户积分排行榜的教程

使用 Redis 实现排行榜功能