基於redis的zSet集合做數據緩存實現分頁查詢 java

      需要場景:最近公司要做手機頁面展示新聞文章數據查詢的優化工作,讓我提個優化方案。現狀是目前手機頁面的數據請求系統後臺,系統後臺然後調用其他系統的接口,返回分頁數據到前臺展示,這樣一來,用戶每次下拉到頁面底部加載更多數據都要調用其他接口,用戶體驗顯然不是很好,那有沒有更好的方案呢?

      優化方案: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結構正是我們想要的緩存類型,我們把分數score用文章的主鍵news_id,把每個文章的的內容用json字符串放入member裏,redis的數據會根據socre也就是news_id自動排序,我們只需要對redis進行新增、刪除的操作就行了(修改可以先刪除再新增)。最後還要考慮redis裏的數據定期刪除的問題,一般來說,設置緩存的過期時間即可,但是設置過期時間是針對key來設置,這裏最好的解決方法就是限制緩存的數據的個數,當數據的個數超過設置的限制個數之後,就是從score最低的值開始刪除即可。也就是score最低的值,也就是news_id按照自增長的規則,最小的news_id的數據就是比較早的數據,。

    補充說明一些Zset裏說數據不重複是指:如果新增一個數據裏的member如果緩存裏存在,這個數據的socre和member就會覆蓋緩存裏的數據,也就是說score是在數據裏會重複,而member在數據裏是不重複的。      

 來一張全部的邏輯圖:


   貼出一些主要代碼:

一、kafka新增數據(keyNewsList爲redis的key值,jm爲文章的json數據格式,redisImpNewsListNum是從配置文件裏取的redis大小的限值)

//創建zset格式的數據,scorenews_iddouble型,member爲每個稿件的數據
double score = Double.parseDouble(jm.getString("news_id"));
redisTemplate.opsForZSet().removeRangeByScore(keyNewsList, score,score);
redisTemplate.opsForZSet().add(keyNewsList, jm.toJSONString(), score);
LOG.debug("rediskafka緩存首次數據成功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_NewReadertrue 爲閱讀數增加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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章