更多Spring文章,歡迎點擊 一灰灰Blog-Spring專題
在一些遊戲和活動中,當涉及到社交元素的時候,排行榜能夠說是一個很常見的需求場景了,就咱們一般見到的排行榜而言,會提供如下基本功能java
上面能夠說是一個排行榜須要實現的幾個基本要素了,正好咱們剛講到了redis這一節,本篇則開始實戰,詳細描述如何藉助redis來實現一份全球排行榜git
<!-- more -->github
在進行方案設計以前,先模擬一個真實的應用場景,而後進行輔助設計與實現redis
之前一段時間特別🔥的跳一跳這個小遊戲進行說明,假設咱們這個遊戲用戶遍及全球,所以咱們要設計一個全球的榜單,每一個玩家都會根據本身的戰績在排行榜中獲取一個排名,咱們須要支持全球榜單的查詢,本身排位的查詢這兩種最基本的查詢場景;此外當個人分數比上一次的高時,我須要更新個人積分,從新得到個人排名;spring
此外也會有一些高級的統計,好比哪一個分段的人數最多,什麼分段是瓶頸點,再根據地理位置計算平均分等等數組
本篇博文主要內容將放在排行榜的設計與實現上;至於高級的功能實現,後續有機會再說安全
由於排行榜的功能比較簡單了,也不須要什麼複雜的結構設計,也沒有什麼複雜的交互,所以咱們須要確認的無非就是數據結構 + 存儲單元數據結構
存儲單元併發
表示排行榜中每一位上應該持有的信息,一個最簡單的以下app
// 用來代表具體的用戶 long userId; // 用戶在排行榜上的排名 long rank; // 用戶的歷史最高積分,也就是排行榜上的積分 long score;
數據結構
排行榜,通常而言都是連續的,藉此咱們能夠聯想到一個合適的數據結構LinkedList,好處在於排名變更時,不須要數組的拷貝
上圖演示,當一個用戶積分改變時,須要向前遍歷找到合適的位置,插入並獲取新的排名, 在更新和插入時,相比較於ArrayList要好不少,但依然有如下幾個缺陷
問題1:用戶如何獲取本身的排名?
使用LinkedList
在更新插入和刪除的帶來優點以外,在隨機獲取元素的支持會差一點,最差的狀況就是從頭至尾進行掃描
問題2:併發支持的問題?
當有多個用戶同時更新score時,併發的更新排名問題就比較突出了,固然可使用jdk中相似寫時拷貝數組的方案
上面是咱們本身來實現這個數據結構時,會遇到的一些問題,固然咱們的主題是藉助redis來實現排行榜,下面則來看下,利用redis能夠怎麼簡單的支持咱們的需求場景
這裏主要使用的是redis的ZSET數據結構,帶權重的集合,下面分析一下可能性
從zset的特性來看,咱們每一個用戶的積分,丟到zset中,就是一個帶權重的元素,並且是已經排好序的了,只須要獲取元素對應的index,就是咱們預期的排名
再具體的實現以前,能夠先查看一下redis中zset的相關方法和操做姿式:SpringBoot高級篇Redis之ZSet數據結構使用姿式
咱們主要是藉助zset提供的一些方法來實現排行榜的需求,下面的具體方法設計中,也會有相關說明
首先準備好redis環境,spring項目搭建好,而後配置好redisTemplate
/** * Created by @author yihui in 15:05 18/11/8. */ public class DefaultSerializer implements RedisSerializer<Object> { private final Charset charset; public DefaultSerializer() { this(Charset.forName("UTF8")); } public DefaultSerializer(Charset charset) { Assert.notNull(charset, "Charset must not be null!"); this.charset = charset; } @Override public byte[] serialize(Object o) throws SerializationException { return o == null ? null : String.valueOf(o).getBytes(charset); } @Override public Object deserialize(byte[] bytes) throws SerializationException { return bytes == null ? null : new String(bytes, charset); } } @Configuration public class AutoConfig { @Bean(value = "selfRedisTemplate") public RedisTemplate<String, String> stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { StringRedisTemplate redis = new StringRedisTemplate(); redis.setConnectionFactory(redisConnectionFactory); // 設置redis的String/Value的默認序列化方式 DefaultSerializer stringRedisSerializer = new DefaultSerializer(); redis.setKeySerializer(stringRedisSerializer); redis.setValueSerializer(stringRedisSerializer); redis.setHashKeySerializer(stringRedisSerializer); redis.setHashValueSerializer(stringRedisSerializer); redis.afterPropertiesSet(); return redis; } }
上傳用戶積分,然而zset中有一點須要注意的是其排行是根據score進行升序排列,這個就和咱們實際的狀況不太同樣了;爲了和實際狀況一致,能夠將score取反;另一個就是排行默認是從0開始的,這個與咱們的實際也不太同樣,須要+1
/** * 更新用戶積分,並獲取最新的我的所在排行榜信息 * * @param userId * @param score * @return */ public RankDO updateRank(Long userId, Float score) { // 由於zset默認積分小的在前面,因此咱們對score進行取反,這樣用戶的積分越大,對應的score越小,排名越高 redisComponent.add(RANK_PREFIX, String.valueOf(userId), -score); Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId)); return new RankDO(rank + 1, score, userId); }
上面的實現,主要利用了zset的兩個方法,一個是添加元素,一個是查詢排名,對應的redis操做方法以下,
@Resource(name = "selfRedisTemplate") private StringRedisTemplate redisTemplate; /** * 添加一個元素, zset與set最大的區別就是每一個元素都有一個score,所以有個排序的輔助功能; zadd * * @param key * @param value * @param score */ public void add(String key, String value, double score) { redisTemplate.opsForZSet().add(key, value, score); } /** * 判斷value在zset中的排名 zrank * * 積分小的在前面 * * @param key * @param value * @return */ public Long rank(String key, String value) { return redisTemplate.opsForZSet().rank(key, value); }
獲取我的排行信息,主要就是兩個一個是排名一個是積分;須要注意的是當用戶沒有積分時(即沒有上榜時),須要額外處理
/** * 獲取用戶的排行榜位置 * * @param userId * @return */ public RankDO getRank(Long userId) { // 獲取排行, 由於默認是0爲開頭,所以實際的排名須要+1 Long rank = redisComponent.rank(RANK_PREFIX, String.valueOf(userId)); if (rank == null) { // 沒有排行時,直接返回一個默認的 return new RankDO(-1L, 0F, userId); } // 獲取積分 Double score = redisComponent.score(RANK_PREFIX, String.valueOf(userId)); return new RankDO(rank + 1, Math.abs(score.floatValue()), userId); }
上面的封裝中,除了使用前面的獲取用戶排名以外,還有獲取用戶積分
/** * 查詢value對應的score zscore * * @param key * @param value * @return */ public Double score(String key, String value) { return redisTemplate.opsForZSet().score(key, value); }
有了前面的基礎以後,這個就比較簡單了,首先獲取用戶的我的排名,而後查詢固定排名段的數據便可
private List<RankDO> buildRedisRankToBizDO(Set<ZSetOperations.TypedTuple<String>> result, long offset) { List<RankDO> rankList = new ArrayList<>(result.size()); long rank = offset; for (ZSetOperations.TypedTuple<String> sub : result) { rankList.add(new RankDO(rank++, Math.abs(sub.getScore().floatValue()), Long.parseLong(sub.getValue()))); } return rankList; } /** * 獲取用戶所在排行榜的位置,以及排行榜中其先後n個用戶的排行信息 * * @param userId * @param n * @return */ public List<RankDO> getRankAroundUser(Long userId, int n) { // 首先是獲取用戶對應的排名 RankDO rank = getRank(userId); if (rank.getRank() <= 0) { // fixme 用戶沒有上榜時,不返回 return Collections.emptyList(); } // 由於實際的排名是從0開始的,因此查詢周邊排名時,須要將n-1 Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX, Math.max(0, rank.getRank() - n - 1), rank.getRank() + n - 1); return buildRedisRankToBizDO(result, rank.getRank() - n); }
看下上面的實現,獲取用戶排名以後,就能夠計算要查詢的排名範圍[Math.max(0, rank.getRank() - n - 1), rank.getRank() + n - 1]
其次須要注意的如何將返回的結果進行封裝,上面寫了個轉換類,主要起始排行榜信息
上面的理解以後,這個就很簡答了
/** * 獲取前n名的排行榜數據 * * @param n * @return */ public List<RankDO> getTopNRanks(int n) { Set<ZSetOperations.TypedTuple<String>> result = redisComponent.rangeWithScore(RANK_PREFIX, 0, n - 1); return buildRedisRankToBizDO(result, 1); }
首先準備一個測試腳本,批量的插入一下積分,用於後續的查詢更新使用
public class RankInitTest { private Random random; private RestTemplate restTemplate; @Before public void init() { random = new Random(); restTemplate = new RestTemplate(); } private int genUserId() { return random.nextInt(1024); } private double genScore() { return random.nextDouble() * 100; } @Test public void testInitRank() { for (int i = 0; i < 30; i++) { restTemplate.getForObject("http://localhost:8080/update?userId=" + genUserId() + "&score=" + genScore(), String.class); } } }
上面執行完畢以後,排行榜中應該就有三十條數據,接下來咱們開始逐個接口測試,首先獲取top10排行
對應的rest接口以下
@RestController public class RankAction { @Autowired private RankListComponent rankListComponent; @GetMapping(path = "/topn") public List<RankDO> showTopN(int n) { return rankListComponent.getTopNRanks(n); } }
接下來咱們挑選第15名,獲取對應的排行榜信息
@GetMapping(path = "/rank") public RankDO queryRank(long userId) { return rankListComponent.getRank(userId); }
首先咱們從redis中獲取第15名的userId,而後再來查詢
而後嘗試修改下他的積分,改大一點,將score改爲80分,則會排到第五名
@GetMapping(path = "/update") public RankDO updateScore(long userId, float score) { return rankListComponent.updateRank(userId, score); }
最後咱們查詢下這個用戶周邊2個的排名信息
@GetMapping(path = "/around") public List<RankDO> around(long userId, int n) { return rankListComponent.getRankAroundUser(userId, n); }
上面利用redis的zset實現了排行榜的基本功能,主要藉助下面三個方法
雖然實現了基本功能,可是問題仍是有很多的
一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
一灰灰blog
知識星球