基于Redis的ZSET實現(xiàn)用戶邀請排行榜
背景
在我們的項目中,有用戶的邀請功能,每一次邀請別人注冊,會有一定的積分,然后我們同時提供了一個排行榜的功能,可以基于這個積分進行排名。
排名的功能比較簡單,就是基于積分去排序就行了,這里面我們利用了Redis的ZSET的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)快速的排序。
因為ZSET是一個天然有序的數(shù)據(jù)結(jié)構(gòu),我們可以把積分當(dāng)做score,用戶id當(dāng)做member,放到zset中,zset會默認按照SCORE進行排序的。
偽代碼實現(xiàn)
以下是用戶接受邀請部分的代碼實現(xiàn):
@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對應(yīng)的鎖進行加鎖操作,避免并發(fā)更新
rLock.lock();
try {
// 獲取邀請者的當(dāng)前排名分數(shù)
Double score = inviteRank.getScore(inviterId);
// 如果當(dāng)前分數(shù)為空,則設(shè)置默認為0.0
if (score == null) {
score = 0.0;
}
// 將邀請者的排名分數(shù)增加100.0,并更新到排行榜中
inviteRank.add(score + 100.0, inviterId);
} finally {
// 最終釋放邀請者ID對應(yīng)的鎖
rLock.unlock();
}
}
這里主要是用到了Redisson的RLock進行了加鎖,并且是用的lock方法,在加鎖失敗時阻塞一直嘗試。主要就是避免多個用戶同時被邀請時,更新分數(shù)會出現(xiàn)并發(fā)而導(dǎo)致分數(shù)累加錯誤。
這里面的排行榜inviteRank,其實是:
private RScoredSortedSet<String> inviteRank;
@Override
public void afterPropertiesSet() throws Exception {
this.inviteRank = redissonClient.getScoredSortedSet("inviteRank");
}在以上邏輯中進行初始化和實例化的,其實他是一個RScoredSortedSet,是一個支持排序的Set,他提供了很多方法可以方便的實現(xiàn)排名的功能,如:
- getScore:獲取指定成員的分數(shù)。
- add:向有序集合中添加一個成員,指定該成員的分數(shù)。
- rank:獲取指定成員在有序集合中的排名(從小到大排序,排名從 0 開始)。
- revRank:獲取指定成員在有序集合中的排名(從大到小排序,排名從 0 開始)。
- entryRange:獲取分數(shù)在指定范圍內(nèi)的成員及其分數(shù)的集合。
比如我們提供了以下幾個和排名有關(guān)的方法,其實就是對上述方法的一些封裝:
//獲取指定用戶的排名,按照分數(shù)從高到低
public Integer getInviteRank(String userId) {
Integer rank = inviteRank.revRank(userId);
if (rank != null) {
return rank + 1;
}
return null;
}//按照分數(shù)從高到低,獲取前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;
}多維度排行榜實現(xiàn)
前面的實現(xiàn)中,如果分數(shù)相同,那么排序的結(jié)果是不確定的,那么如果我們想要實現(xiàn)多維度排名,即先按照分數(shù)排,分數(shù)相同的話按照上榜時間排,如何實現(xiàn)呢?
為了實現(xiàn)分數(shù)相同按照時間順序排序,我們可以將分數(shù)score設(shè)置為一個浮點數(shù),其中整數(shù)部分為得分,小數(shù)部分為時間戳,如下所示:
score = 分數(shù) + 時間戳/1e13
假設(shè)現(xiàn)在的時間戳是1680417299000,除以1e13得到0.1680417299000,再加上一個固定的分數(shù)(比如10),那么最終的分數(shù)就是10.1680417299000,可以將它作為zset中某個成員的分數(shù),用來排序。
這么做了之后,假如有四個數(shù)字:
10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011
他們按照倒序拍完順序之后,會是:
11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000
實現(xiàn)了分數(shù)倒序排列,分數(shù)相同時間戳大(上榜更晚的)的排在了前面,這和我們的需求相反了,所以,就需要在做一次轉(zhuǎn)換。
score = 分數(shù) + 1-時間戳/1e13
因為時間戳是這種形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13個0,所以用時間戳/1e13就能得到一個小數(shù)
這樣可以保證分數(shù)相同時,按照時間戳從小到大排序,即先得分的先被排在前面。
修改后的代碼如下:
private void updateInviteRank(String inviterId) {
if (inviterId == null) {
return;
}
//1、這里因為是一個私有方法,無法通過注解方式實現(xiàn)分布式鎖。
//2、register方法已經(jīng)加了鎖,這里需要二次加鎖的原因是register鎖的是注冊人,這里鎖的是邀請人
RLock rLock = redissonClient.getLock(inviterId);
rLock.lock();
try {
//獲取當(dāng)前用戶的積分
Double score = inviteRank.getScore(inviterId);
if (score == null) {
score = 0.0;
}
//獲取最近一次上榜時間
long currentTimeStamp = System.currentTimeMillis();
//把上榜時間轉(zhuǎn)成小數(shù)(時間戳13位,所以除以10000000000000能轉(zhuǎn)成小數(shù)),并且倒序排列(用1減),即上榜時間越早,分數(shù)越大(時間越晚,時間戳越大,用1減一下,就反過來了)
double timePartScore = 1 - (double) currentTimeStamp / 10000000000000L;
//1、當(dāng)前積分保留整數(shù),即移除上一次的小數(shù)位
//2、當(dāng)前積分加100,表示新邀請了一個用戶
//3、加上“最近一次上榜時間的倒序小數(shù)位“作為score
inviteRank.add(score.intValue() + 100.0 + timePartScore, inviterId);
} finally {
rLock.unlock();
}
}到此這篇關(guān)于基于Redis的ZSET實現(xiàn)用戶邀請排行榜的文章就介紹到這了,更多相關(guān)Redis ZSET用戶邀請排行榜內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決redis-cli報錯Could not connect to Redis&
這篇文章主要介紹了解決redis-cli報錯Could not connect to Redis at 127.0.0.1:6379: Connection refused,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04

