前言
我相信大部分人,乃至公司和團隊在設計排行榜都考慮的是redis,zadd操作,不需要排序,維護獲取,操作都極其簡單;
無一例外我也是;
在專案中運營了大量的模板,來處理各個木塊的排行榜資訊;
統一的會在晚上又一些結算處理;就牽涉一次性拉取,過濾,發放獎勵,甚至還有刪除操作;
同時為了節約運營陳本,定期還需要對redis裡面無效資料進行刪除,收縮操作;
目前redis已經達到6.55g,這只是其中一個平臺運營部署;
運營過專案的都知道雲redis的費用是非常高昂的;所以就需要定期做一些無用資料的刪除操作;
可是問題來了,隨著運營時間的推移,每天早上總能看到如此的異常推送;
就問你揪心不?仔細一查才發現還是資料庫相關的操作導致的卡主了;
於是下定決心對排行榜等相關功能進行調整;
削峰
1. 首先就是對資料庫的一些刪除操作,集中早上5-6點去處理他;這個時候線上玩家最少;處理一些垃圾資料最為合理;基本卡了也不會對玩家的感覺特別強烈反饋;
於是把原來處理的資料移除的程式碼調整為定時器建立非同步任務在凌晨5-6點去處理;
2. 第二步就是為了避免玩家集體拉取資料,並對資料進行操作;考慮重構排行榜程式碼需求,
排行榜自定義實現
為了模擬排行榜實現的,
首先應該考慮,排行榜資料,既方便查詢修改,又方便排序;
那麼肯定需要考慮的是
一個map,用於存取和玩家相關積分資料;
一個list,用於排序積分;
首先我們來實現一個積分類
1 package org.wxd.lang.rank; 2 3 import lombok.Getter; 4 import lombok.Setter; 5 import lombok.experimental.Accessors; 6 import org.wxd.io.ObjectFactory; 7 import org.wxd.timer.TimeUtil; 8 9 import java.util.Comparator; 10 import java.util.Objects; 11 12 /** 13 * 排行 14 * 15 * @author: Troy.Chen(無心道, 15388152619) 16 * @version: 2022-12-08 21:27 17 **/ 18 @Getter 19 @Setter 20 @Accessors(chain = true) 21 public class RankScore implements Comparable<RankScore> { 22 23 /** 正序 */ 24 public static final Comparator<RankScore> Sort = (o1, o2) -> { 25 if (o1.score != o2.score) { 26 return Double.compare(o1.score, o2.score); 27 } 28 29 if (o1.scoreTime != o2.scoreTime) { 30 /**時間取值要倒敘*/ 31 return Long.compare(o2.scoreTime, o1.scoreTime); 32 } 33 34 return Long.compare(o1.uid, o2.uid); 35 }; 36 37 /** 倒敘 */ 38 public static final Comparator<RankScore> BreSort = (o1, o2) -> { 39 if (o1.score != o2.score) { 40 return Double.compare(o1.score, o2.score); 41 } 42 43 if (o1.scoreTime != o2.scoreTime) { 44 /**時間取值要倒敘*/ 45 return Long.compare(o2.scoreTime, o1.scoreTime); 46 } 47 48 return Long.compare(o1.uid, o2.uid); 49 }; 50 51 private long uid; 52 private double score; 53 private long scoreTime; 54 55 public RankScore setScore(double score) { 56 this.score = score; 57 this.scoreTime = TimeUtil.currentTimeMillis(); 58 return this; 59 } 60 61 @Override public int compareTo(RankScore o2) { 62 return Sort.compare(this, o2); 63 } 64 65 public int scoreIntValue() { 66 return (int) score; 67 } 68 69 public long scoreLongValue() { 70 return (long) score; 71 } 72 73 @Override public boolean equals(Object o) { 74 if (this == o) return true; 75 if (o == null || getClass() != o.getClass()) return false; 76 RankScore rankScore = (RankScore) o; 77 return uid == rankScore.uid; 78 } 79 80 @Override public int hashCode() { 81 return Objects.hash(uid); 82 } 83 84 @Override public String toString() { 85 return ObjectFactory.stringBuilder(sb -> { 86 sb.append(this.getClass().getSimpleName()).append("{"); 87 sb.append("uid=").append(uid); 88 sb.append(", score=").append(score); 89 sb.append('}'); 90 }); 91 } 92 }
接下來我實現程式碼
1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.ArrayList; 9 import java.util.HashMap; 10 import java.util.List; 11 12 /** 13 * @author: Troy.Chen(無心道, 15388152619) 14 * @version: 2022-12-08 20:48 15 **/ 16 public class RankPackTest { 17 18 public class RankPack { 19 HashMap<Long, RankScore> rankMap = new HashMap<>(); 20 List<Long> rankList = new ArrayList<>(); 21 22 public void addScore(long uid, double score) { 23 /*忽律併發問題 可以自行改為 ConcurrentHashMap*/ 24 rankMap.computeIfAbsent(uid, l -> { 25 RankScore rankScore = new RankScore().setUid(uid).setScore(score); 26 /*能到這裡初始化,那麼list裡面必然也沒有資料*/ 27 rankList.add(uid); 28 return rankScore; 29 }); 30 } 31 32 public void sort() { 33 /*忽律併發問題*/ 34 rankList.sort((o1, o2) -> { 35 RankScore r1 = rankMap.get(o1); 36 RankScore r2 = rankMap.get(o2); 37 return r1.compareTo(r2); 38 }); 39 } 40 41 public void breSort() { 42 /*忽律併發問題*/ 43 rankList.sort((o1, o2) -> { 44 RankScore r1 = rankMap.get(o1); 45 RankScore r2 = rankMap.get(o2); 46 return r2.compareTo(r1); 47 }); 48 } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 init(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 } 64 65 public int randomScore() { 66 return RandomUtils.random(100, 10000); 67 } 68 69 public void init() { 70 MarkTimer build = MarkTimer.build(); 71 int speed = 1; 72 for (int i = 0; i < 300; i++) { 73 rankPack.addScore(speed, randomScore()); 74 speed++; 75 } 76 rankPack.breSort(); 77 float v = build.execTime(); 78 System.out.println(rankPack.getClass().getSimpleName() + " - 數量:" + rankPack.rankMap.size() + ", 全部排序耗時:" + v); 79 show(); 80 } 81 82 public void sort() { 83 MarkTimer build = MarkTimer.build(); 84 rankPack.rankMap.values().forEach(rankScore -> rankScore.setScore(randomScore())); 85 rankPack.breSort(); 86 float v = build.execTime(); 87 System.out.println(rankPack.getClass().getSimpleName() + " - 數量:" + rankPack.rankMap.size() + ", 全部排序耗時:" + v); 88 show(); 89 } 90 91 public void sort2() { 92 MarkTimer build = MarkTimer.build(); 93 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 94 randomItem.setScore(randomScore()); 95 rankPack.breSort(); 96 float v = build.execTime(); 97 show(); 98 int index = rankPack.rankList.indexOf(randomItem.getUid()); 99 System.out.println(v + " - " + randomItem + " - 當前排名:" + (index + 1)); 100 } 101 102 public void show() { 103 // AtomicInteger atomicInteger = new AtomicInteger(); 104 // for (int i = 0; i < 10; i++) { 105 // Long uid = rankPack.rankList.get(i); 106 // RankScore rankScore = rankPack.rankMap.get(uid); 107 // System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 108 // } 109 } 110 111 }
執行一下看看效果
視乎效能還可以,等等,我們是不是忽律一個問題,現在排行榜只有300個物件哦,加到3萬試試
很明顯看得出來,全排序情況下直接翻了30倍左右;單個項修改後排序直接翻了差不多40倍;
到這裡或許有很多已經滿足需求了,;
可是我的需求遠不止如此,這很明顯時間消耗太久了,不知道有麼有更為適合的方案來處理效能;
突然想到一個問題,或許瞭解樹結構的同學知道,樹裝結構資料,在插入資料的時候就已經排序了,並且根據hash索引,效能非常高;
由此我們想到,
那麼我們排序很耗時;我能不能不做這個排序操作,透過索引資料結構來達到目的呢?
改造樹裝結構
我們把剛才hashmap改為treemap
吧list改為treeset試試效果
測試一下,感覺確實是快了很多呢
我們來列印一下資料看看情況
哦豁,為什麼資料不對;
不是我們預期的效果,
然後研究了treeset資料結構就知道,它是排序的,但是他是add的時候排序的,一旦add就不再變更了;
那我們能不能嘗試修改的時候先移除,再修改,然後在add呢?
我們來改造一下addScore方法塊;
排序正常了
可以看出執行結構,整體差距不算特別大;
如果考慮併發效能問題;可以把
TreeMap 換成 ConcurrentSkipListMap
TreeSet 換成 ConcurrentSkipListSet
java自帶的跳錶結構,新增刪除查詢,都會非常高效;
最後貼上一下最新的全部測試程式碼
1 package test; 2 3 import org.junit.Test; 4 import org.wxd.lang.RandomUtils; 5 import org.wxd.lang.rank.RankScore; 6 import org.wxd.system.MarkTimer; 7 8 import java.util.concurrent.ConcurrentSkipListMap; 9 import java.util.concurrent.ConcurrentSkipListSet; 10 11 /** 12 * @author: Troy.Chen(無心道, 15388152619) 13 * @version: 2022-12-08 20:48 14 **/ 15 public class RankPackTest { 16 17 public class RankPack { 18 ConcurrentSkipListMap<Long, RankScore> rankMap = new ConcurrentSkipListMap<>(); 19 ConcurrentSkipListSet<RankScore> rankList = new ConcurrentSkipListSet<>(); 20 21 public void addScore(long uid, double score) { 22 /*忽律併發問題 可以自行改為 ConcurrentHashMap*/ 23 RankScore score1 = rankMap.computeIfAbsent(uid, l -> { 24 RankScore rankScore = new RankScore().setUid(uid); 25 return rankScore; 26 }); 27 rankList.remove(score1); 28 score1.setScore(score); 29 rankList.add(score1); 30 } 31 32 // public void sort() { 33 // /*忽律併發問題*/ 34 // rankList.sort((o1, o2) -> { 35 // RankScore r1 = rankMap.get(o1); 36 // RankScore r2 = rankMap.get(o2); 37 // return r1.compareTo(r2); 38 // }); 39 // } 40 // 41 // public void breSort() { 42 // /*忽律併發問題*/ 43 // rankList.sort((o1, o2) -> { 44 // RankScore r1 = rankMap.get(o1); 45 // RankScore r2 = rankMap.get(o2); 46 // return r2.compareTo(r1); 47 // }); 48 // } 49 50 } 51 52 RankPack rankPack = new RankPack(); 53 54 @Test 55 public void test() { 56 sort(); 57 sort(); 58 sort(); 59 sort(); 60 sort2(); 61 sort2(); 62 sort2(); 63 sort2(); 64 } 65 66 public int randomScore() { 67 return RandomUtils.random(100, 10000); 68 } 69 70 public void sort() { 71 MarkTimer build = MarkTimer.build(); 72 int speed = 1; 73 for (int i = 0; i < 30000; i++) { 74 rankPack.addScore(speed, randomScore()); 75 speed++; 76 } 77 // rankPack.breSort(); 78 float v = build.execTime(); 79 System.out.println(rankPack.getClass().getSimpleName() + " - 數量:" + rankPack.rankList.size() + ", 全部排序耗時:" + v); 80 show(); 81 } 82 83 public void sort2() { 84 MarkTimer build = MarkTimer.build(); 85 RankScore randomItem = (RankScore) RandomUtils.randomItem(rankPack.rankMap.values()); 86 rankPack.addScore(randomItem.getUid(), randomScore()); 87 // rankPack.breSort(); 88 float v = build.execTime(); 89 show(); 90 int index = 0; 91 for (RankScore rankScore : rankPack.rankList) { 92 if (rankScore.getUid() == randomItem.getUid()) break; 93 index++; 94 } 95 System.out.println(v + " - " + randomItem + " - 當前排名:" + (index + 1)); 96 } 97 98 public void show() { 99 int i = 0; 100 for (RankScore rankScore : rankPack.rankList) { 101 System.out.println(rankScore.toString() + " - 排名:" + (i + 1)); 102 i++; 103 if (i >= 10) break; 104 } 105 } 106 107 }
大家如果有興趣可以自己測試哦;
好了這裡提供集中思路來處理排行榜相關的資料;以及排名功能;
不知道園子裡的朋友們,還有沒有更好的思路;
聽說大家喜歡看,機會是留給有耐心的人