在王者榮耀 target=_blank class=infotextkey>王者榮耀中,我們會打排位賽,而且大家最關注的往往都是你的段位,還有在好友中的排名。
作為程序員的你,是否思考過這個段位排行榜是怎么實現的?了解它的實現原理,會不會對上分有所幫助?
一、設計方案
從技術角度而言,我們可以根據排行榜的類型來選擇不同技術方案來進行排行榜設計。
1、數據庫直接排序
在低數據量場景中,用數據庫直接排序做排行榜的,有很多。
舉個例子,比如要做一個程序員薪資排行榜,看看哪個城市的程序員最有錢。
根據某招聘網站的數據,2023年中國國內程序員的平均月薪為1.2萬元,其中最高的是北京,達到了2.1萬元,最低的是西安,只有0.7萬元。
以下是幾個主要城市的程序員平均月薪排行榜:
- 北京:2.1萬元
- 上海:1.9萬元
- 深圳:1.8萬元
- 杭州:1.6萬元
- 廣州:1.5萬元
- 成都:1.3萬元
- 南京:1.2萬元
- 武漢:1.1萬元
- 西安:0.7萬元
從這個榜單中可以看出,我拖了大家的后腿,抱歉了。
對于這種量級的數據,加好索引,用好top,都不會超過100ms,在請求量小、數據量小的情況下,用數據庫做排行榜是完全沒有問題的。
2、王者榮耀好友排行
這類榜單是根據自己好友數據來進行排行的,這類榜單不用將每位好友的數據都存儲在數據庫中,而是通過獲取自己的好友列表,獲取好友的實時分數,在客戶端本地進行本地排序,展現出王者榮耀好友排行榜。
因為向數據庫拉取數據是需要時間的,比如一分鐘拉取一次,因為并非實時拉取,這類榜單對數據庫的壓力還是較小的。
下面探索一下在JAVA中使用redis實現高性能的排行榜是如何實現的?
二、Redis實現計數器
1、什么是計數器功能?
計數器是一種常見的功能,用于記錄某種事件的發生次數。在應用中,計數器可以用來跟蹤用戶行為、統計點擊次數、瀏覽次數等。
例如,您可以使用計數器來記錄一篇文章被閱讀的次數,或者統計某個產品被購買的次數。通過跟蹤計數,您可以了解數據的變化趨勢,從而做出更明智的決策。
2、Redis實現計數器的原理
Redis是一款高性能的內存數據庫,提供了豐富的數據結構和命令,非常適合實現計數器功能。在Redis中,我們可以使用字符串數據類型以及相關的命令來實現計數器。
1)使用INCR命令實現計數器
Redis的INCR命令是一個原子操作,用于將存儲在鍵中的數字遞增1。如果鍵不存在,將會創建并初始化為0,然后再執行遞增操作。這使得我們可以輕松地實現計數器功能。
讓我們通過Java代碼來演示如何使用Redis的INCR命令實現計數器:
import redis.clients.jedis.Jedis;
public class CounterExample {
public static void mAIn(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String articleId = "article:123";
String viewsKey = "views:" + articleId;
// 使用INCR命令遞增計數
long views = jedis.incr(viewsKey);
System.out.println("Article views: " + views);
jedis.close();
}
}
在上面的代碼中,我們使用了Jedis客戶端庫來連接Redis服務器,并使用INCR命令遞增一個存儲在views:article:123鍵中的計數器。每次執行該代碼,計數器的值都會遞增,并且我們可以輕松地獲取到文章的瀏覽次數。
2)使用INCRBY命令實現計數器
除了單次遞增1,我們還可以使用INCRBY命令一次性增加指定的數量。這對于一些需要一次性增加較大數量的場景非常有用。
讓我們繼續使用上面的例子,但這次我們使用INCRBY命令來增加瀏覽次數:
import redis.clients.jedis.Jedis;
public class CounterExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String articleId = "article:123";
String viewsKey = "views:" + articleId;
// 使用INCRBY命令遞增計數
long views = jedis.incrBy(viewsKey, 10); // 一次增加10
System.out.println("Article views: " + views);
jedis.close();
}
}
在上述代碼中,我們使用了INCRBY命令將文章瀏覽次數一次性增加了10。這在統計需要一次性增加較多計數的場景中非常有用。
通過使用Redis的INCR和INCRBY命令,我們可以輕松實現高性能的計數器功能。這些命令的原子性操作保證了計數的準確性,而且非常適用于需要頻繁更新計數的場景。
三、通過Redis實現“王者榮耀”排行榜?
王者榮耀的排行榜是不是用Redis做的,我不得而知,但我的項目中,排行榜確實是用Redis做的,這是實打實的。
1、什么是排行榜功能?
排行榜是一種常見的功能,用于記錄某種項目的排名情況,通常按照某種規則對項目進行排序。在社交媒體、游戲、電商等領域,排行榜功能廣泛應用,可以增強用戶的參與度和競爭性。例如,社交媒體平臺可以通過排行榜展示最活躍的用戶,游戲中可以展示玩家的分數排名等。
2、Redis實現排行榜的原理
在Redis中,我們可以使用有序集合(Sorted Set)數據結構來實現高效的排行榜功能。有序集合是一種鍵值對的集合,每個成員都與一個分數相關聯,Redis會根據成員的分數進行排序。這使得我們能夠輕松地實現排行榜功能。
1)使用ZADD命令添加成員和分數
Redis的ZADD命令用于向有序集合中添加成員和對應的分數。如果成員已存在,可以更新其分數。讓我們通過Java代碼演示如何使用ZADD命令來添加成員和分數到排行榜:
import redis.clients.jedis.Jedis;
public class LeaderboardExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";
// 使用ZADD命令添加成員和分數
jedis.zadd(leaderboardKey, 1000, player1);
jedis.zadd(leaderboardKey, 800, player2);
jedis.close();
}
}
在上述代碼中,我們使用ZADD命令將PlayerA和PlayerB作為成員添加到leaderboard有序集合中,并分別賦予分數。這樣,我們就在排行榜中創建了兩名玩家的記錄。
2)使用ZINCRBY命令更新成員分數
除了添加成員,我們還可以使用ZINCRBY命令更新已有成員的分數。這在實時更新排行榜中的分數非常有用。
讓我們繼續使用上面的例子,但這次我們將使用ZINCRBY命令來增加玩家的分數:
import redis.clients.jedis.Jedis;
public class LeaderboardExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";
// 使用ZINCRBY命令更新成員分數
jedis.zincrby(leaderboardKey, 200, player1); // 增加200分
jedis.close();
}
}
在上述代碼中,我們使用了ZINCRBY命令將PlayerA的分數增加了200分。這種方式可以用于記錄玩家的得分、積分等變化,從而實時更新排行榜數據。
通過使用Redis的有序集合以及ZADD、ZINCRBY等命令,我們可以輕松實現高性能的排行榜功能。這些命令的原子性操作保證了排行的準確性和一致性,非常適用于需要頻繁更新排行榜的場景。
四、計數器與排行榜的性能優化
在本節中,我們將重點討論如何在高并發場景下優化計數器和排行榜功能的性能。通過合理的策略和技巧,我們可以確保系統在處理大量數據和用戶請求時依然保持高性能。
1、如何優化計數器的性能?
1)使用Redis事務
在高并發場景下,多個用戶可能同時對同一個計數器進行操作,這可能引發并發沖突。為了避免這種情況,可以使用Redis的事務來確保原子性操作。事務將一組命令包裝在一個原子性的操作中,保證這些命令要么全部執行成功,要么全部不執行。
下面是一個示例,演示如何使用Redis事務進行計數器操作:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
public class CounterOptimizationExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String counterKey = "view_count";
try {
// 開始事務
Transaction tx = jedis.multi();
// 對計數器執行加1操作
tx.incr(counterKey);
// 執行事務
tx.exec();
} catch (JedisException e) {
// 處理事務異常
e.printStackTrace();
} finally {
jedis.close();
}
}
}
在上述代碼中,我們使用了Jedis客戶端庫,通過MULTI命令開啟一個事務,然后在事務中執行INCR命令來增加計數器的值。最后,使用EXEC命令執行事務。如果在事務執行期間出現錯誤,我們可以通過捕獲JedisException來處理異常。
2)使用分布式鎖
另一種優化計數器性能的方法是使用分布式鎖。分布式鎖可以確保在同一時刻只有一個線程能夠對計數器進行操作,避免了并發沖突。這種機制可以保證計數器的更新是串行化的,從而避免了競爭條件。
以下是一個使用Redisson框架實現分布式鎖的示例:
import org.redisson.Redisson;
import org.redisson.api.RLock;
public class CounterOptimizationWithLockExample {
public static void main(String[] args) {
Redisson redisson = Redisson.create();
RLock lock = redisson.getLock("counter_lock");
try {
lock.lock(); // 獲取鎖
// 執行計數器操作
} finally {
lock.unlock(); // 釋放鎖
redisson.shutdown();
}
}
}
在上述代碼中,我們使用了Redisson框架來創建一個分布式鎖。通過調用lock.lock()獲取鎖,然后執行計數器操作,最后通過lock.unlock()釋放鎖。這樣可以保證在同一時間只有一個線程能夠執行計數器操作。
2、如何優化排行榜的性能?
1)分頁查詢
在排行榜中,通常會有大量的數據,如果一次性查詢所有數據,可能會影響性能。為了解決這個問題,可以使用分頁查詢。將排行榜數據分成多個頁,每次查詢一小部分數據,以減輕數據庫的負擔。
以下是一個分頁查詢排行榜的示例:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class LeaderboardPaginationExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String leaderboardKey = "leaderboard";
int pageSize = 10; // 每頁顯示的數量
int pageIndex = 1; // 頁碼
// 獲取指定頁的排行榜數據
Set<Tuple> leaderboardPage = jedis.zrevrangeWithScores(leaderboardKey, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1);
for (Tuple tuple : leaderboardPage) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}
jedis.close();
}
}
在上述代碼中,我們使用zrevrangeWithScores命令來獲取指定頁的排行榜數據。通過計算起始索引和結束索引,我們可以實現分頁查詢功能。
2)使用緩存
為了進一步提高排行榜的查詢性能,可以將排行榜數據緩存起來,減少對數據庫的訪問。例如,可以使用Redis緩存最近的排行榜數據,定期更新緩存以保持數據的新鮮性。
以下是一個緩存排行榜數據的示例:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class LeaderboardCachingExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String leaderboardKey = "leaderboard";
String cacheKey = "cached_leaderboard";
int cacheExpiration = 300; // 緩存過期時間,單位:秒
// 嘗試從緩存中獲取排行榜數據
Set<Tuple> cachedLeaderboard = jedis.zrevrangeWithScores(cacheKey, 0, -1);
if (cachedLeaderboard.isEmpty()) {
// 如果緩存為空,從數據庫獲取數據并更新緩存
Set<Tuple> leaderboardData = jedis.zrevrangeWithScores(leaderboardKey, 0, -1);
jedis.zadd(cacheKey, leaderboardData);
jedis.expire(cacheKey, cacheExpiration);
cachedLeaderboard = leaderboardData;
}
for
(Tuple tuple : cachedLeaderboard) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}
jedis.close();
}
}
在上述代碼中,我們首先嘗試從緩存中獲取排行榜數據。如果緩存為空,我們從數據庫獲取數據,并將數據存入緩存。使用expire命令來設置緩存的過期時間,以保持數據的新鮮性。
五、實際應用案例
在本節中,我們將通過兩個實際的案例,展示如何使用Redis的計數器和排行榜功能來構建社交媒體點贊系統和游戲玩家排行榜系統。這些案例將幫助您更好地理解如何將Redis的功能應用于實際場景中。
1、社交媒體點贊系統案例
1)問題背景
假設我們要構建一個社交媒體平臺,用戶可以在文章、照片等內容上點贊。我們希望能夠統計每個內容的點贊數量,并實時顯示最受歡迎的內容。
2)系統架構
- 每個內容的點贊數可以使用Redis的計數器功能進行維護。
- 我們可以使用有序集合(Sorted Set)來維護內容的排名信息,將內容的點贊數作為分數。
3)數據模型
- 每個內容都有一個唯一的標識,如文章ID或照片ID。
- 使用一個計數器來記錄每個內容的點贊數。
- 使用一個有序集合來記錄內容的排名,以及與內容標識關聯的分數。
4)Redis操作步驟
- 用戶點贊時,使用Redis的INCR命令增加對應內容的點贊數。
- 使用ZADD命令將內容的標識和點贊數作為分數添加到有序集合中。
Java代碼示例:
import redis.clients.jedis.Jedis;
public class SocialMediaLikeSystem {
private Jedis jedis;
public SocialMediaLikeSystem() {
jedis = new Jedis("localhost", 6379);
}
public void likeContent(String contentId) {
// 增加點贊數
jedis.incr("likes:" + contentId);
// 更新排名信息
jedis.zincrby("rankings", 1, contentId);
}
public long getLikes(String contentId) {
return Long.parseLong(jedis.get("likes:" + contentId));
}
public void showRankings() {
// 顯示排名信息
System.out.println("Top content rankings:");
jedis.zrevrangeWithScores("rankings", 0, 4)
.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
public static void main(String[] args) {
SocialMediaLikeSystem system = new SocialMediaLikeSystem();
system.likeContent("post123");
system.likeContent("post456");
system.likeContent("post123");
System.out.println("Likes for post123: " + system.getLikes("post123"));
System.out.println("Likes for post456: " + system.getLikes("post456"));
system.showRankings();
}
}
在上述代碼中,我們創建了一個名為SocialMediaLikeSystem的類來模擬社交媒體點贊系統。我們使用了Jedis客戶端庫來連接到Redis服務器,并實現了點贊、獲取點贊數和展示排名的功能。
每當用戶點贊時,我們會使用INCR命令遞增點贊數,并使用ZINCRBY命令更新有序集合中的排名信息。通過調用zrevrangeWithScores命令,我們可以獲取到點贊數排名前幾的內容。
2、游戲玩家排行榜案例
1)問題背景
在一個多人在線游戲中,我們希望能夠實時追蹤和顯示玩家的排行榜,以鼓勵玩家參與并提升游戲的競爭性。
2)系統架構
- 每個玩家的得分可以使用Redis的計數器功能進行維護。
- 我們可以使用有序集合來維護玩家的排名,將玩家的得分作為分數。
3)數據模型
- 每個玩家都有一個唯一的ID。
- 使用一個計數器來記錄每個玩家的得分。
- 使用一個有序集合來記錄玩家的排名,以及與玩家ID關聯的得分。
4)Redis操作步驟
- 玩家完成游戲時,使用Redis的ZINCRBY命令增加玩家的得分。
- 使用ZREVRANK命令獲取玩家的排名。
5)Java代碼示例
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;
public class GameLeaderboard {
private Jedis jedis;
public GameLeaderboard() {
jedis = new Jedis("localhost", 6379);
}
public void updateScore(String playerId, double score) {
jedis.zincrby("leaderboard", score, playerId);
}
public Long getPlayerRank(String playerId) {
return jedis.zrevrank("leaderboard", playerId);
}
public Set<Tuple> getTopPlayers(int count) {
return jedis.zrevrangeWithScores("leaderboard", 0, count - 1);
}
public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
leaderboard.updateScore("player123", 1500);
leaderboard.updateScore("player456", 1800);
leaderboard.updateScore("player789", 1600);
Long rank = leaderboard.getPlayerRank("player456");
System.out.println("Rank of player456: " + (rank != null ? rank + 1 : "Not ranked"));
Set<Tuple> topPlayers = leaderboard.getTopPlayers(3);
System.out.println("Top players:");
topPlayers.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
}
在上述代碼中,我們創建了一個名為GameLeaderboard的類來模擬游戲玩家排行榜系統。我們同樣使用Jedis客戶端庫來連接到Redis服務器,并實現了更新玩家得分、獲取玩家排名和獲取排名前幾名玩家的功能。
使用zincrby命令可以更新玩家的得分,而zrevrank命令則用于獲取玩家的排名,注意排名從0開始計數。通過調用zrevrangeWithScores命令,我們可以獲取到排名前幾名玩家以及他們的得分。
六、總結與最佳實踐
在本篇博客中,我們深入探討了如何使用Redis構建高性能的計數器和排行榜功能。通過實際案例和詳細的Java代碼示例,我們了解了如何在實際應用中應用這些功能,提升系統性能和用戶體驗。讓我們在這一節總結Redis在計數器和排行榜功能中的價值,并提供一些最佳實踐指南。
1、Redis在計數器和排行榜中的價值
通過使用Redis的計數器和排行榜功能,我們可以實現以下價值:
- 實時性和高性能:Redis的內存存儲和優化的數據結構使得計數器和排行榜功能能夠以極高的性能實現。這對于需要實時更新和查詢數據的場景非常重要。
- 用戶參與度提升:在社交媒體和游戲等應用中,計數器和排行榜功能可以激勵用戶參與。通過顯示點贊數量或排行榜,用戶感受到了更強的互動性和競爭性,從而增加了用戶參與度。
- 數據統計和分析:通過統計計數和排行數據,我們可以獲得有價值的數據洞察。這些數據可以用于分析用戶行為、優化內容推薦等,從而指導業務決策。
2、最佳實踐指南
以下是一些使用Redis構建計數器和排行榜功能的最佳實踐指南:
- 合適的數據結構選擇:根據實際需求,選擇合適的數據結構。計數器可以使用簡單的String類型,而排行榜可以使用有序集合(Sorted Set)來存儲數據。
- 保證數據準確性:在高并發環境下,使用Redis的事務、管道和分布式鎖來保證計數器和排行榜的數據準確性。避免并發寫入導致的競爭條件。
- 定期數據清理:定期清理不再需要的計數器和排行數據,以減小數據量和提高查詢效率??梢允褂肸REMRANGEBYRANK命令來移除排行榜中的過期數據。
- 適度的緩存:對于排行榜數據,可以考慮添加適度的緩存以提高查詢效率。但要注意平衡緩存的更新和數據的一致性。
通過遵循這些最佳實踐,您可以更好地應用Redis的計數器和排行榜功能,為您的應用程序帶來更好的性能和用戶體驗。