基于Redis的ZSET实现用户邀请排行榜
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">背景</a></li><li><a href="#_label1">伪代码实现</a></li><li><a href="#_label2">多维度排行榜实现</a></li></ul></div><p class="maodian"><a name="_label0"></a></p><h2>背景</h2><p>在我们的项目中,有用户的邀请功能,每一次邀请别人注册,会有一定的积分,然后我们同时提供了一个排行榜的功能,可以基于这个积分进行排名。</p>
<p>排名的功能比较简单,就是基于积分去排序就行了,这里面我们利用了Redis的ZSET的数据结构实现快速的排序。</p>
<p>因为ZSET是一个天然有序的数据结构,我们可以把积分当做score,用户id当做member,放到zset中,zset会默认按照SCORE进行排序的。</p>
<p class="maodian"><a name="_label1"></a></p><h2>伪代码实现</h2>
<p>以下是用户接受邀请部分的代码实现:</p>
<div class="jb51code"><pre class="brush:java;">@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);
//其他逻辑
}</pre></div>
<p>updateInviteRank的额代码逻辑如下:</p>
<div class="jb51code"><pre class="brush:java;">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();
}
}
</pre></div>
<p>这里主要是用到了Redisson的RLock进行了加锁,并且是用的lock方法,在加锁失败时阻塞一直尝试。主要就是避免多个用户同时被邀请时,更新分数会出现并发而导致分数累加错误。</p>
<p>这里面的排行榜inviteRank,其实是:</p>
<div class="jb51code"><pre class="brush:java;">private RScoredSortedSet<String> inviteRank;
@Override
public void afterPropertiesSet() throws Exception {
this.inviteRank = redissonClient.getScoredSortedSet("inviteRank");
}</pre></div>
<p>在以上逻辑中进行初始化和实例化的,其实他是一个RScoredSortedSet,是一个支持排序的Set,他提供了很多方法可以方便的实现排名的功能,如:</p>
<ul><li><strong>getScore:</strong>获取指定成员的分数。</li><li><strong>add:</strong>向有序集合中添加一个成员,指定该成员的分数。</li><li><strong>rank</strong>:获取指定成员在有序集合中的排名(从小到大排序,排名从 0 开始)。</li><li><strong>revRank:</strong>获取指定成员在有序集合中的排名(从大到小排序,排名从 0 开始)。</li><li><strong>entryRange</strong>:获取分数在指定范围内的成员及其分数的集合。</li></ul>
<p>比如我们提供了以下几个和排名有关的方法,其实就是对上述方法的一些封装:</p>
<div class="jb51code"><pre class="brush:java;">//获取指定用户的排名,按照分数从高到低
public Integer getInviteRank(String userId) {
Integer rank = inviteRank.revRank(userId);
if (rank != null) {
return rank + 1;
}
return null;
}</pre></div>
<div class="jb51code"><pre class="brush:java;">//按照分数从高到低,获取前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;
}</pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>多维度排行榜实现</h2>
<p>前面的实现中,如果分数相同,那么排序的结果是不确定的,那么如果我们想要实现多维度排名,即先按照分数排,分数相同的话按照上榜时间排,如何实现呢?</p>
<p>为了实现分数相同按照时间顺序排序,<strong>我们可以将分数score设置为一个浮点数,其中整数部分为得分,小数部分为时间戳</strong>,如下所示:</p>
<blockquote><p>score = 分数 + 时间戳/1e13</p></blockquote>
<p>假设现在的时间戳是1680417299000,除以1e13得到0.1680417299000,再加上一个固定的分数(比如10),那么最终的分数就是10.1680417299000,可以将它作为zset中某个成员的分数,用来排序。</p>
<p>这么做了之后,假如有四个数字:</p>
<p>10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011</p>
<p>他们按照倒序拍完顺序之后,会是:</p>
<p>11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000</p>
<p>实现了分数倒序排列,分数相同时间戳大(上榜更晚的)的排在了前面,这和我们的需求相反了,所以,就需要在做一次转换。</p>
<blockquote><p>score = 分数 + 1-时间戳/1e13</p></blockquote>
<p>因为时间戳是这种形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13个0,所以用时间戳/1e13就能得到一个小数</p>
<p>这样可以保证分数相同时,按照时间戳从小到大排序,即先得分的先被排在前面。</p>
<p>修改后的代码如下:</p>
<div class="jb51code"><pre class="brush:java;">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();
}
}</pre></div>
頁:
[1]