基于Redis的ZSET實(shí)現(xiàn)用戶邀請(qǐng)排行榜
背景
在我們的項(xiàng)目中,有用戶的邀請(qǐng)功能,每一次邀請(qǐng)別人注冊(cè),會(huì)有一定的積分,然后我們同時(shí)提供了一個(gè)排行榜的功能,可以基于這個(gè)積分進(jìn)行排名。
排名的功能比較簡(jiǎn)單,就是基于積分去排序就行了,這里面我們利用了Redis的ZSET的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)快速的排序。
因?yàn)閆SET是一個(gè)天然有序的數(shù)據(jù)結(jié)構(gòu),我們可以把積分當(dāng)做score,用戶id當(dāng)做member,放到zset中,zset會(huì)默認(rèn)按照SCORE進(jìn)行排序的。
偽代碼實(shí)現(xiàn)
以下是用戶接受邀請(qǐng)部分的代碼實(shí)現(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();
}
}
//用戶注冊(cè)
//更新排名
updateInviteRank(inviterId);
//其他邏輯
}updateInviteRank的額代碼邏輯如下:
private void updateInviteRank(String inviterId) {
// 如果邀請(qǐng)者ID為空,則直接返回,不進(jìn)行操作
if (inviterId == null) {
return;
}
// 獲取Redisson的鎖對(duì)象
RLock rLock = redissonClient.getLock(inviterId);
// 對(duì)邀請(qǐng)者ID對(duì)應(yīng)的鎖進(jìn)行加鎖操作,避免并發(fā)更新
rLock.lock();
try {
// 獲取邀請(qǐng)者的當(dāng)前排名分?jǐn)?shù)
Double score = inviteRank.getScore(inviterId);
// 如果當(dāng)前分?jǐn)?shù)為空,則設(shè)置默認(rèn)為0.0
if (score == null) {
score = 0.0;
}
// 將邀請(qǐng)者的排名分?jǐn)?shù)增加100.0,并更新到排行榜中
inviteRank.add(score + 100.0, inviterId);
} finally {
// 最終釋放邀請(qǐng)者ID對(duì)應(yīng)的鎖
rLock.unlock();
}
}
這里主要是用到了Redisson的RLock進(jìn)行了加鎖,并且是用的lock方法,在加鎖失敗時(shí)阻塞一直嘗試。主要就是避免多個(gè)用戶同時(shí)被邀請(qǐng)時(shí),更新分?jǐn)?shù)會(huì)出現(xiàn)并發(fā)而導(dǎo)致分?jǐn)?shù)累加錯(cuò)誤。
這里面的排行榜inviteRank,其實(shí)是:
private RScoredSortedSet<String> inviteRank;
@Override
public void afterPropertiesSet() throws Exception {
this.inviteRank = redissonClient.getScoredSortedSet("inviteRank");
}在以上邏輯中進(jìn)行初始化和實(shí)例化的,其實(shí)他是一個(gè)RScoredSortedSet,是一個(gè)支持排序的Set,他提供了很多方法可以方便的實(shí)現(xiàn)排名的功能,如:
- getScore:獲取指定成員的分?jǐn)?shù)。
- add:向有序集合中添加一個(gè)成員,指定該成員的分?jǐn)?shù)。
- rank:獲取指定成員在有序集合中的排名(從小到大排序,排名從 0 開始)。
- revRank:獲取指定成員在有序集合中的排名(從大到小排序,排名從 0 開始)。
- entryRange:獲取分?jǐn)?shù)在指定范圍內(nèi)的成員及其分?jǐn)?shù)的集合。
比如我們提供了以下幾個(gè)和排名有關(guān)的方法,其實(shí)就是對(duì)上述方法的一些封裝:
//獲取指定用戶的排名,按照分?jǐn)?shù)從高到低
public Integer getInviteRank(String userId) {
Integer rank = inviteRank.revRank(userId);
if (rank != null) {
return rank + 1;
}
return null;
}//按照分?jǐn)?shù)從高到低,獲取前N個(gè)用戶的排名信息
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;
}多維度排行榜實(shí)現(xiàn)
前面的實(shí)現(xiàn)中,如果分?jǐn)?shù)相同,那么排序的結(jié)果是不確定的,那么如果我們想要實(shí)現(xiàn)多維度排名,即先按照分?jǐn)?shù)排,分?jǐn)?shù)相同的話按照上榜時(shí)間排,如何實(shí)現(xiàn)呢?
為了實(shí)現(xiàn)分?jǐn)?shù)相同按照時(shí)間順序排序,我們可以將分?jǐn)?shù)score設(shè)置為一個(gè)浮點(diǎn)數(shù),其中整數(shù)部分為得分,小數(shù)部分為時(shí)間戳,如下所示:
score = 分?jǐn)?shù) + 時(shí)間戳/1e13
假設(shè)現(xiàn)在的時(shí)間戳是1680417299000,除以1e13得到0.1680417299000,再加上一個(gè)固定的分?jǐn)?shù)(比如10),那么最終的分?jǐn)?shù)就是10.1680417299000,可以將它作為zset中某個(gè)成員的分?jǐn)?shù),用來排序。
這么做了之后,假如有四個(gè)數(shù)字:
10.1680417299000、10.1680417299011、11.1680417299000、11.1680417299011
他們按照倒序拍完順序之后,會(huì)是:
11.1680417299011>11.1680417299000>10.1680417299011>10.1680417299000
實(shí)現(xiàn)了分?jǐn)?shù)倒序排列,分?jǐn)?shù)相同時(shí)間戳大(上榜更晚的)的排在了前面,這和我們的需求相反了,所以,就需要在做一次轉(zhuǎn)換。
score = 分?jǐn)?shù) + 1-時(shí)間戳/1e13
因?yàn)闀r(shí)間戳是這種形式1708746590000 ,共有13位,而1e13是10000000000000,即1后面13個(gè)0,所以用時(shí)間戳/1e13就能得到一個(gè)小數(shù)
這樣可以保證分?jǐn)?shù)相同時(shí),按照時(shí)間戳從小到大排序,即先得分的先被排在前面。
修改后的代碼如下:
private void updateInviteRank(String inviterId) {
if (inviterId == null) {
return;
}
//1、這里因?yàn)槭且粋€(gè)私有方法,無法通過注解方式實(shí)現(xiàn)分布式鎖。
//2、register方法已經(jīng)加了鎖,這里需要二次加鎖的原因是register鎖的是注冊(cè)人,這里鎖的是邀請(qǐng)人
RLock rLock = redissonClient.getLock(inviterId);
rLock.lock();
try {
//獲取當(dāng)前用戶的積分
Double score = inviteRank.getScore(inviterId);
if (score == null) {
score = 0.0;
}
//獲取最近一次上榜時(shí)間
long currentTimeStamp = System.currentTimeMillis();
//把上榜時(shí)間轉(zhuǎn)成小數(shù)(時(shí)間戳13位,所以除以10000000000000能轉(zhuǎn)成小數(shù)),并且倒序排列(用1減),即上榜時(shí)間越早,分?jǐn)?shù)越大(時(shí)間越晚,時(shí)間戳越大,用1減一下,就反過來了)
double timePartScore = 1 - (double) currentTimeStamp / 10000000000000L;
//1、當(dāng)前積分保留整數(shù),即移除上一次的小數(shù)位
//2、當(dāng)前積分加100,表示新邀請(qǐng)了一個(gè)用戶
//3、加上“最近一次上榜時(shí)間的倒序小數(shù)位“作為score
inviteRank.add(score.intValue() + 100.0 + timePartScore, inviterId);
} finally {
rLock.unlock();
}
}到此這篇關(guān)于基于Redis的ZSET實(shí)現(xiàn)用戶邀請(qǐng)排行榜的文章就介紹到這了,更多相關(guān)Redis ZSET用戶邀請(qǐng)排行榜內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mac下設(shè)置redis開機(jī)啟動(dòng)方法步驟
這篇文章主要介紹了mac下設(shè)置redis開機(jī)啟動(dòng),本文詳細(xì)的給出了操作步驟,需要的朋友可以參考下2015-07-07
redis分布式Jedis類型轉(zhuǎn)換的異常深入研究
這篇文章主要介紹了redis分布式Jedis類型轉(zhuǎn)換的異常深入研究,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03
SpringMVC集成redis配置的多種實(shí)現(xiàn)方法
這篇文章主要介紹了SpringMVC集成redis配置的多種實(shí)現(xiàn)方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Redis設(shè)置鍵的生存時(shí)間或過期時(shí)間的方法詳解
這篇文章主要介紹了Redis如何設(shè)置鍵的生存時(shí)間或過期時(shí)間,通過EXPIRE命令或者PEXIPIRE命令,客戶端可以以秒或者毫秒精度為數(shù)據(jù)庫(kù)中的某個(gè)鍵設(shè)置生存時(shí)間,文中有詳細(xì)的代碼供供大家參考,需要的朋友可以參考下2024-03-03
詳解Centos7下配置Redis并開機(jī)自啟動(dòng)
本篇文章主要介紹了Centos7下配置Redis并開機(jī)自啟動(dòng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-11-11

