基于Redis的ZSET实现用户邀请排行榜
作者:埃泽漫笔
背景
在我们的项目中,有用户的邀请功能,每一次邀请别人注册,会有一定的积分,然后我们同时提供了一个排行榜的功能,可以基于这个积分进行排名。
排名的功能比较简单,就是基于积分去排序就行了,这里面我们利用了Redis的ZSET的数据结构实现快速的排序。
因为ZSET是一个天然有序的数据结构,我们可以把积分当做score,用户id当做member,放到zset中,zset会默认按照SCORE进行排序的。
伪代码实现
以下是用户接受邀请部分的代码实现:
@DistributeLock(keyExpression = "#telephone", scene = "USER_REGISTER")
public UserOperatorResponse register(String telephone, String inviteCode) {
//用户名生成
String inviterId = null;
if (StringUtils.isNotBlank(inviteCode)) {
User inviter = userMapper.findByInviteCode(inviteCode);
if (inviter != null) {
inviterId = inviter.getId().toString();
}
}
//用户注册
//更新排名
updateInviteRank(inviterId);
//其他逻辑
}updateInviteRank的额代码逻辑如下:
private void updateInviteRank(String inviterId) {
// 如果邀请者ID为空,则直接返回,不进行操作
if (inviterId == null) {
return;
}
// 获取Redisson的锁对象
RLock rLock = redissonClient.getLock(inviterId);
// 对邀请者ID对应的锁进行加锁操作,避免并发更新
rLock.lock();
try {
// 获取邀请者的当前排名分数
Double score = inviteRank.getScore(inviterId);
// 如果当前分数为空,则设置默认为0.0
if (score == null) {
score = 0.0;
}
// 将邀请者的排名分数增加100.0,并更新到排行榜中
inviteRank.add(score + 100.0, inviterId);
} finally {
// 最终释放邀请者ID对应的锁
rLock.unlock();
}
}
这里主要是用到了Redisson的RLock进行了加锁,并且是用的lock方法,在加锁失败时阻塞一直尝试。主要就是避免多个用户同时被邀请时,更新分数会出现并发而导致分数累加错误。
这里面的排行榜inviteRank,其实是:
private RScoredSortedSet<String> inviteRank;
@Override
public void afterPropertiesSet() throws Exception {
this.inviteRank = redissonClient.getScoredSortedSet("inviteRank");
}在以上逻辑中进行初始化和实例化的,其实他是一个RScoredSortedSet,是一个支持排序的Set,他提供了很多方法可以方便的实现排名的功能,如:
- getScore:获取指定成员的分数。
- add:向有序集合中添加一个成员,指定该成员的分数。
- rank:获取指定成员在有序集合中的排名(从小到大排序,排名从 0 开始)。
- revRank:获取指定成员在有序集合中的排名(从大到小排序,排名从 0 开始)。
- entryRange:获取分数在指定范围内的成员及其分数的集合。
比如我们提供了以下几个和排名有关的方法,其实就是对上述方法的一些封装:
//获取指定用户的排名,按照分数从高到低
public Integer getInviteRank(String userId) {
Integer rank = inviteRank.revRank(userId);
if (rank != null) {
return rank + 1;
}
return null;
}//按照分数从高到低,获取前N个用户的排名信息
public List<InviteRankInfo> getTopN(Integer topN) {
Collection<ScoredEntry<String>> rankInfos = inviteRank.entryRangeReversed(0, topN - 1);
List<InviteRankInfo> inviteRankInfos = new ArrayList<>();
if (rankInfos != null) {
for (ScoredEntry<String> rankInfo : rankInfos) {
InviteRankInfo inviteRankInfo = new InviteRankInfo();
String userId = rankInfo.getValue();
if (StringUtils.isNotBlank(userId)) {
User user = findById(Long.valueOf(userId));
if (user != null) {
inviteRankInfo.setNickName(user.getNickName());
inviteRankInfo.setInviteCode(user.getInviteCode());
inviteRankInfo.setInviteCount(rankInfo.getScore().intValue() / 100);
inviteRankInfos.add(inviteRankInfo);
}
}
}
}
return inviteRankInfos;
}多维度排行榜实现
前面的实现中,如果分数相同,那么排序的结果是不确定的,那么如果我们想要实现多维度排名,即先按照分数排,分数相同的话按照上榜时间排,如何实现呢?
为了实现分数相同按照时间顺序排序,我们可以将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳,如下所示:
score = 分数 + 时间戳/1e13
假设现在的时间戳是1680417299000,除以1e13得到0.1680417299000,再加上一个固定的分数(比如10),那么最终的分数就是10.1680417299000,可以将它作为zset中某个成员的分数,用来排序。
这么做了之后,假如有四个数字:
10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011
他们按照倒序拍完顺序之后,会是:
11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000
实现了分数倒序排列,分数相同时间戳大(上榜更晚的)的排在了前面,这和我们的需求相反了,所以,就需要在做一次转换。
score = 分数 + 1-时间戳/1e13
因为时间戳是这种形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13个0,所以用时间戳/1e13就能得到一个小数
这样可以保证分数相同时,按照时间戳从小到大排序,即先得分的先被排在前面。
修改后的代码如下:
private void updateInviteRank(String inviterId) {
if (inviterId == null) {
return;
}
//1、这里因为是一个私有方法,无法通过注解方式实现分布式锁。
//2、register方法已经加了锁,这里需要二次加锁的原因是register锁的是注册人,这里锁的是邀请人
RLock rLock = redissonClient.getLock(inviterId);
rLock.lock();
try {
//获取当前用户的积分
Double score = inviteRank.getScore(inviterId);
if (score == null) {
score = 0.0;
}
//获取最近一次上榜时间
long currentTimeStamp = System.currentTimeMillis();
//把上榜时间转成小数(时间戳13位,所以除以10000000000000能转成小数),并且倒序排列(用1减),即上榜时间越早,分数越大(时间越晚,时间戳越大,用1减一下,就反过来了)
double timePartScore = 1 - (double) currentTimeStamp / 10000000000000L;
//1、当前积分保留整数,即移除上一次的小数位
//2、当前积分加100,表示新邀请了一个用户
//3、加上“最近一次上榜时间的倒序小数位“作为score
inviteRank.add(score.intValue() + 100.0 + timePartScore, inviterId);
} finally {
rLock.unlock();
}
}到此这篇关于基于Redis的ZSET实现用户邀请排行榜的文章就介绍到这了,更多相关Redis ZSET用户邀请排行榜内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
