需要場景:最近公司要做手機頁面展示新聞文章數據查詢的優化工作,讓我提個優化方案。現狀是目前手機頁面的數據請求系統後臺,系統後臺然後調用其他系統的接口,返回分頁數據到前臺展示,這樣一來,用戶每次下拉到頁面底部加載更多數據都要調用其他接口,用戶體驗顯然不是很好,那有沒有更好的方案呢?
優化方案:redis正好適合在這種場景下使用,用戶每次下拉到頁面底部,此時從前臺頁面到系統後臺分頁(假如每次取10條)取數據,可以直接到redis裏取數據,如果redis返回的數據爲空或者小於你要取的10條數據,那麼調用接口取10條分頁數據放入緩存,然後再從緩存裏取數據返回到前臺。這樣的話,只有當其中一個用戶第一次查詢的時候會調用接口數據存入緩存,以後這個用戶或者其他用戶再看這個文章信息的時候,就是直接從緩存裏取數據,就相當快捷,提高用戶體驗。經測試,之前每次調用接口在700~800ms左右,現在每次從緩存裏取數據,只需要200ms左右,性能顯然提升很大。
前面只提到下拉到頁面底部加載更多數據時的情況,其實我們當刷新最新的數據時,這時候該怎麼處理呢?事實上系統後臺用到了kafka消費者接收從其他後臺實時發送的文章數據,這裏接收的文章有三種類型:一種是add,就說明這個文章是新增的發佈到手機頁面的數據;一種是update,就說明這個文章是要更新已經發布的數據,最後一種是del,就說明這個文章是要從手機頁面刪除的。也就是說,我們一方面可以從接口獲取歷史的數據,另一方面可以實時獲取最新的被髮送來的新增文章數據(或者是要修改和刪除的)。另外補充一點,爲了提升用戶一打開手機就能快速的看到新聞信息的體驗度,我們在系統啓動成功後,默認先調用接口存入緩存10條記錄,這樣,用戶第一次進入手機頁面默認就能先從緩存裏取10條新聞信息。
上面說了那麼多業務,無非是兩點,一:從緩存裏獲取分頁數據;二:對緩存數據進行增刪改查的操作。而redis定義了5種數據結構,這5種數據結構類型分別爲String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。
下面來對這5種數據結構類型作簡單的介紹(表格引用https://www.jianshu.com/p/7bf5dc61ca06文章裏的):
結構 類型 | 結構存儲的值 | 結構的讀寫能力 |
---|---|---|
String | 可以是字符串、整數或者浮點數 | 對整個字符串或者字符串的其中一部分執行操作;對象和浮點數執行自增(increment)或者自減(decrement) |
List | 一個鏈表,鏈表上的每個節點都包含了一個字符串 | 從鏈表的兩端推入或者彈出元素;根據偏移量對鏈表進行修剪(trim);讀取單個或者多個元素;根據值來查找或者移除元素 |
Set | 包含字符串的無序收集器(unorderedcollection),並且被包含的每個字符串都是獨一無二的、各不相同 | 添加、獲取、移除單個元素;檢查一個元素是否存在於某個集合中;計算交集、並集、差集;從集合裏賣弄隨機獲取元素 |
Hash | 包含鍵值對的無序散列表 | 添加、獲取、移除單個鍵值對;獲取所有鍵值對 |
Zset | 字符串成員(member)與浮點數分值(score)之間的有序映射,元素的排列順序由分值的大小決定 | 添加、獲取、刪除單個元素;根據分值範圍(range)或者成員來獲取元素 |
補充說明一些Zset裏說數據不重複是指:如果新增一個數據裏的member如果緩存裏存在,這個數據的socre和member就會覆蓋緩存裏的數據,也就是說score是在數據裏會重複,而member在數據裏是不重複的。
來一張全部的邏輯圖:
貼出一些主要代碼:
一、kafka新增數據(keyNewsList爲redis的key值,jm爲文章的json數據格式,redisImpNewsListNum是從配置文件裏取的redis大小的限值)
//創建zset格式的數據,score爲news_id的double型,member爲每個稿件的數據 double score = Double.parseDouble(jm.getString("news_id")); redisTemplate.opsForZSet().removeRangeByScore(keyNewsList, score,score); redisTemplate.opsForZSet().add(keyNewsList, jm.toJSONString(), score); LOG.debug("redis從kafka緩存首次數據成功score:"+score+",key:"+keyNewsList+",member:"+jm.toString()); //測試 System.out.println("新增之後的個數" + redisTemplate.opsForZSet().zCard(keyNewsList)); //追加邏輯:限制keyNewsList的個數 String redisImpNewsListNum = ApplicationSetting.getProperty("redis.impnews.listNum"); if (StringUtils.isNotBlank(redisImpNewsListNum)){ //配置文件裏設置個數限制 Long redisImpNewsListNumLong=Long.parseLong(redisImpNewsListNum); //keyNewsList的個數 Long keyNewsListSize=redisTemplate.opsForZSet().zCard(keyNewsList); //如果keyNewsList的個數 超過 設置的限制的話,從socre最小的值開始刪除 if (keyNewsListSize > redisImpNewsListNumLong){ redisTemplate.opsForZSet().removeRange(keyNewsList,0,keyNewsListSize-redisImpNewsListNumLong-1); LOG.debug("redis裏的keyNewsList的個數:"+keyNewsListSize+",超過設置的限值redis.impnews.listNum:"+redisImpNewsListNumLong+",刪除超出的數據。"); } } System.out.println("刪除之後的個數" + redisTemplate.opsForZSet().zCard(keyNewsList));
二、kafka更新數據
//先刪除後新增 redisTemplate.opsForZSet().removeRangeByScore(keyNewsList, score,score); Boolean aBoolean= redisTemplate.opsForZSet().add(keyNewsList, jo.toJSONString(), score); if (aBoolean){ LOG.debug("kafka更新數據,update成功,key:"+keyNewsList+",score:"+score+",member:"+jo.toJSONString()); }
三、kafka刪除數據
//通過score來刪除緩存裏的數據 Double score = Double.parseDouble(data.getString("news_id")); redisTemplate.opsForZSet().removeRangeByScore(keyNewsList, score,score); LOG.debug("kafka刪除數據,直接delete成功,key:" + keyNewsList + ",score:" + score);
四、系統首次加載存入緩存數據
BSPResponse bspRes = bspClient.getList("", "topmaceco,topcptmkt,topmoney,topfxmkt,topbond,topcom", "1", "100", "","0",""); //String keyNewsList = "newsList_redis_*"; String keyNewsList = "newsList_redis_impNews"; LOG.info("redis首頁要聞請求bsp接口狀態:"+bspRes.getMessage()); if (bspRes.isSuccess()) { JSONArray ja = bspRes.getBodyResult().getJSONArray("LIST"); List<JSONObject> obj = new ArrayList<JSONObject>(); if(null != ja && ja.size() > 0){ for (int i = 0; i < ja.size(); i++) { JSONObject jm = (JSONObject) ja.get(i); obj.add(jm); } } if (obj != null&& obj.size()>0) { //清除所有 redisTemplate.opsForZSet().removeRange(keyNewsList,0,-1); for (int i = 0; i < obj.size(); i++) { JSONObject jm = obj.get(i); String news_id = jm.getString("news_id"); String info_id = jm.getString("info_id"); //判斷資訊閱讀數是否應該增加 //is_NewReader爲true 爲閱讀數增加1 String is_NewReader = "true"; String keyName_1 = ""; jm.put("is_newreader", is_NewReader); jm.put("flag", ""); //接口有摘要(news_abst),作者(author), 正文length(data_content_size)、 可分享字段(is_share) // 返回給終端的字段有:摘要(news_abst),作者(author), 是否有正文(hasContent)、 是否可分享(isShare) jm.put("isShare","0".equals(jm.getString("is_share"))?false:true); jm.put("hasContent","0".equals(jm.getString("data_content_size"))? false:true); jm.put("news_type", jm.getString("info_type")==null?"":jm.getString("info_type")); //放入緩存裏(防止數據重複,先刪除在新增) Double score = Double.parseDouble(news_id); redisTemplate.opsForZSet().removeRangeByScore(keyNewsList, score,score); redisTemplate.opsForZSet().add(keyNewsList,jm.toJSONString(),score); } LOG.info("redis首頁要聞緩存:" + redisTemplate.opsForZSet().reverseRange(keyNewsList,0,-1)); } } else { LOG.error("redis首頁要聞請求bsp接口返回失敗"); }
五、前臺調用系統後臺
說明:page_news_id是前臺傳遞到後臺的最小news_id,根據這值,我們可以定位到緩存的數據位置,然後開始取多條數據。
舉例:注意在score 在redis裏是double類型
score member
44390 {“news_id”:44390,"title":............}
44389 {“news_id”:44389,"title":............}
44385 {“news_id”:44385,"title":............}
44378 {“news_id”:44378,"title":............}
44376 {“news_id”:44376,"title":............}
44374 {“news_id”:44374,"title":............}
44373 {“news_id”:44373,"title":............}
44372 {“news_id”:44372,"title":............}
44370 {“news_id”:44370,"title":............}
44369 {“news_id”:44369,"title":............}
44367 {“news_id”:44367,"title":............}
44365 {“news_id”:44365,"title":............}
.... ........
假如說前臺app展示數據已經到44376了,當他下拉數據調用後臺接口傳遞參數page_news_id=44376,pageSize=5,
那麼利用reverseRangeByscore(keyNewsList,0,pageScore,1,pageSize)方法,取到的數據就會按照score從大到小排序(RangeByscore是按照從小到大排序):
第一個參數 表示 keyNewsList是key,你要從哪個緩存取數;
第二第三個參數 表示 0 pageScore 表示從socre範圍最小是0,最大是pageScore;
第四第五個參數 表示 你要從數據下標開始從1取到pageSize,你要取多個。如果從0開始就會把44376這條數據也會取出來,所以要從1開始取。
取出的結果就是如下數據:
44374 {“news_id”:44374,"title":............}
44373 {“news_id”:44373,"title":............}
44372 {“news_id”:44372,"title":............}
44370 {“news_id”:44370,"title":............}
44369 {“news_id”:44369,"title":............}
if(StringUtils.isNotBlank(page_news_id)){ System.out.println("下滑分頁加載數據"); //下滑分頁加載數據 pageScore = Double.parseDouble(page_news_id); System.out.println("pageScore = " + pageScore); set = redisTemplate.opsForZSet().reverseRangeByScore(keyNewsList,0,pageScore,1,pageSize); System.out.println("緩存數據大小前"+set.size()); //緩存裏沒有數據,則調用渠道整合接口向緩存裏插入數據 if (set == null || set.size()<10){ bspDataAddToRedis(user_id, classify_code, page_num, page_size,delay, page_news_id,keyNewsList,pageScore,pageSize); set = redisTemplate.opsForZSet().reverseRangeByScore(keyNewsList,0,pageScore,1,pageSize); System.out.println("緩存數據大小後"+set.size()); } } else { System.out.println("前臺首次加載和下拉刷新最新數據"); set = redisTemplate.opsForZSet().reverseRange(keyNewsList,0,pageSize-1); }
參考資料:
https://www.jianshu.com/p/7bf5dc61ca06
https://www.cnblogs.com/knowledgesea/p/4999288.html
https://my.oschina.net/1107156537/blog/1617252