Redis系列(二)--緩存設(shè)計(整表緩存以及排行榜緩存方案實(shí)現(xiàn))

抱歉,其實(shí)這篇應(yīng)該幾天前就出的,不過在這個項(xiàng)目中的lucene加載出bug了(雖然還沒解決,但緩存的先記錄下來,發(fā)出給大家)。

本系列:

(1)SSM框架構(gòu)建積分系統(tǒng)和基本商品檢索系統(tǒng)(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(1)框架整合構(gòu)建

(2)SSM框架構(gòu)建積分系統(tǒng)和基本商品檢索系統(tǒng)(Spring+SpringMVC+MyBatis+Lucene+Redis+MAVEN)(2)建立商品數(shù)據(jù)庫和Lucene的搭建

(3)Redis系列(一)--安裝、helloworld以及讀懂配置文件


文章結(jié)構(gòu):(1)整表緩存;(2)排行榜緩存方案。

Redis文檔


一、整表緩存:(演示禁言表的整表緩存)

整表緩存核心思想:我們使用的是普通的key-value數(shù)據(jù)結(jié)構(gòu)。key對應(yīng)我們普通的禁言id。value則對應(yīng)這個id禁言過他人的列表的list。利用jsonarray和list之間的互換。形成這一整表緩存策略!!!

list里面則裝載著禁言表的記錄,每一行記錄對應(yīng)一個Gag。

(1)先編寫好我們的接口

public interface JedisClient {

    public String get(String key);
    public String set(String key, String value);
    public String hget(String hkey, String key);
    public long hset(String hkey, String key, String value);
    public long incr(String key);
    public long expire(String key, int second);
    public long ttl(String key);
    public long del(String key);
    public long hdel(String hkey, String key);
    public long zadd(String key,double score,User user);
    public Set<String> zgetAll(String key, long start, long end);
    //拿整個榜單
    public long zaddList(String key,List<User> userList);
    //拿去榜單最后一名,比如前50名,這里就拿到第50名的User。積分變化時就拿這個值去判斷,大于就丟user進(jìn)redis,小于則不管
    public Set<String> getTopLast(String key,long start,long end);
}

(2)編寫業(yè)務(wù)接口實(shí)現(xiàn)類:

@Service
public class JedisClientSingle implements JedisClient {

    @Autowired
    private JedisPool jedisPool;

    @Override
    public String get(String key) {
        Jedis jedis = jedisPool.getResource();
        String string = jedis.get(key);
        jedis.close();
        return string;
    }

    @Override
    public String set(String key, String value) {
        Jedis jedis = jedisPool.getResource();
        String string = jedis.set(key, value);
        jedis.close();
        return string;
    }
//最基本的數(shù)據(jù)類型操作,key-value
    @Override
    public String hget(String hkey, String key) {
        System.out.println("jedisPool   " + jedisPool);
        Jedis jedis = jedisPool.getResource();
        System.out.println("jedis   " + jedis);
        String string = jedis.hget(hkey, key);
        jedis.close();
        return string;
    }

    @Override
    public long hset(String hkey, String key, String value) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.hset(hkey, key, value);
        jedis.close();
        return result;
    }

    @Override
    public long incr(String key) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.incr(key);
        jedis.close();
        return result;
    }

    @Override
    public long expire(String key, int second) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.expire(key, second);
        jedis.close();
        return result;
    }

    @Override
    public long ttl(String key) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.ttl(key);
        jedis.close();
        return result;
    }

    @Override
    public long del(String key) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.del(key);
        jedis.close();
        return result;
    }

    @Override
    public long hdel(String hkey, String key) {
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.hdel(hkey, key);
        jedis.close();
        return result;
    }
    @Override
    public long zaddList(String key,List<User> userList){
        Jedis jedis = jedisPool.getResource();
        Long result = null;
        for (int i=0;i<userList.size();i++){
             result = jedis.zadd(key, userList.get(i).getScore(), JsonUtils.objectToJson(userList.get(i)));
        }
        jedis.close();
        return result;
    }

    @Override
    public long zadd(String key,double score,User user){
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.zadd(key, score, JsonUtils.objectToJson(user));
        jedis.close();
        return result;
    }
    @Override
    public Set<String> zgetAll(String key,long start,long end){
        Jedis jedis = jedisPool.getResource();
        Set<String> userSet =jedis.zrange(key, start, end);
        System.out.println("userSet    :"+userSet);
        jedis.close();
        return userSet;
    }
    //拿最后一名的,所以start跟end必須標(biāo)記最后一名的位置索引
    @Override
    public Set<String> getTopLast(String key,long start,long end) {
        Jedis jedis = jedisPool.getResource();
        Set<String> userSet = jedis.zrange(key,start,end);
        System.out.println("userSet    :"+userSet);
        jedis.close();
        return userSet;
    }

}

(3)我們寫進(jìn)緩存的string和讀出來的string必須使用統(tǒng)一的轉(zhuǎn)換方式:我這里使用的是fastjson這個庫

public class JsonUtils {
//轉(zhuǎn)成jsonarray
    public static String objectToJson(Object data) {
        String json = JSONArray.toJSONString(data);
        return json;
    }
    //string轉(zhuǎn)java對象
    public static User jsonObjectToUser(String userString) {
        User user = JSONObject.parseObject(userString.toString(),User.class);
        return user;
    }
    public static List<User> objectToUser(JSONArray array){
        List<User> userList =new ArrayList<>();
        for (int i=0;i<array.size();i++) {
            System.out.print("array.get(i)    "+array.get(i).toString());
            //parseObject解析的是一個字符串,強(qiáng)轉(zhuǎn)成為String是不行的,必須打印成String語句
            User user = JSONObject.parseObject(array.get(i).toString(),User.class);
            userList.add(user);
        }
        return userList;
    }
}

(4)整表緩存的controller調(diào)用:

@RestController
@RequestMapping("/gag")
public class GagController {
    @Autowired
    private GagService gagService;
    @Autowired
    private JedisClient jedisClient;

    @RequestMapping(value = "/gagList",produces="text/html;charset=UTF-8", method = {RequestMethod.GET,RequestMethod.GET})
    public String testRedis(Long id){
        List<Gag> gagList= null;
        try {
            String resulthget = jedisClient.hget("禁言表", id + "");
            if (resulthget != null) {
                //字符串轉(zhuǎn)為list
                System.out.println("有緩存啦啦啦!!!");
                JSONArray array = JSONArray.parseArray(resulthget);
                gagList = (List) array;
            } else {
                System.out.println("禁言表沒查過");//沒有查過就讀取整個表的list出來
                gagList= gagService.findByUserId(id);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            String cacheString = JsonUtils.objectToJson(gagList);//讀取出來后就加載緩存進(jìn)去,但是要轉(zhuǎn)化給json
            jedisClient.hset("禁言表", id + "", cacheString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return JSON.toJSONString(gagList);
    }
}
這里寫圖片描述

二、排行榜緩存方案(重頭戲!!!)

方案一:(優(yōu)雅方案):

1.第一次訪問的時候,查數(shù)據(jù)庫,查整個user表查出topN(使用sql排序),丟給redis(使用sorted set數(shù)據(jù)類型)。

2.排序在redis,redis自動排序。以后的用戶訪問:均訪問redis。

3.只要每次積分變化判斷的時候拿topN的最后一個判別,大于某個閾值則整個user丟進(jìn)redis排序。

效率性能再優(yōu)化:用戶積分變動的時候,(守護(hù)線程)服務(wù)器預(yù)存一下變化的數(shù)量。。到一定量再通知。

4.積分變換的時候都往redis拿個第50名的積分進(jìn)行判斷。再往下去設(shè)定一個小距離為閾值。比如現(xiàn)在第50名的積分是100,那80分一下的應(yīng)該就沒必要扔給redis了吧?

注意:這個排行榜的用戶是會不斷增加的,比如1億用戶,如果剛開始只有前50,后5千萬人的積分大于第50名,那么就會往redis加入這個用戶的信息。(雖然看起來要存很多,其實(shí)一億用戶怎么存也就1G左右的內(nèi)存,簡單暴力優(yōu)雅方案了)

實(shí)現(xiàn):

(1)繼續(xù)沿用JedisClient接口

  //拿整個榜單
public Set<String> zgetAll(String key, long start, long end);
  //批量加入用戶
    public long zaddList(String key,List<User> userList);
    //拿去榜單最后一名,比如前50名,這里就拿到第50名的User。積分變化時就拿這個值去判斷,大于就丟user進(jìn)redis,小于則不管
    public Set<String> getTopLast(String key,long start,long end);

(2)繼續(xù)沿用JedisClientSingle實(shí)現(xiàn)類

//批量加入用戶到redis
@Override
    public long zaddList(String key,List<User> userList){
        Jedis jedis = jedisPool.getResource();
        Long result = null;
        for (int i=0;i<userList.size();i++){
             result = jedis.zadd(key, userList.get(i).getScore(), JsonUtils.objectToJson(userList.get(i)));
        }
        jedis.close();
        return result;
    }
//單個用戶加入。而且可用于更新操作!!!!
    @Override
    public long zadd(String key,double score,User user){
        Jedis jedis = jedisPool.getResource();
        Long result = jedis.zadd(key, score, JsonUtils.objectToJson(user));
        jedis.close();
        return result;
    }
    //全拿榜單信息(拿多少就多少。)
    @Override
    public Set<String> zgetAll(String key,long start,long end){
        Jedis jedis = jedisPool.getResource();
        Set<String> userSet =jedis.zrange(key, start, end);
        System.out.println("userSet    :"+userSet);
        jedis.close();
        return userSet;
    }
    //拿最后一名的,所以start跟end必須標(biāo)記最后一名的位置索引
    @Override
    public Set<String> getTopLast(String key,long start,long end) {
        Jedis jedis = jedisPool.getResource();
        Set<String> userSet = jedis.zrange(key,start,end);
        System.out.println("userSet    :"+userSet);
        jedis.close();
        return userSet;
    }

(3)緩存加入,查詢調(diào)用:(繼續(xù)沿用上面的JsonUtils)

@RequestMapping(value = "/queryTopN", produces = "text/html;charset=UTF-8", method = {RequestMethod.GET, RequestMethod.GET})
    public String queryTopN() {
        List<User> userList = null;
        try {
        //拿出redis的排行榜結(jié)果
            Set<String> resultSet = jedisClient.zgetAll("Toptest", (long) 0, (long) 20);
            System.out.println("resultSet    " + resultSet.toString());
            Iterator<String> iter = resultSet.iterator();//遍歷它
            if (resultSet.size() > 0) {
                //字符串轉(zhuǎn)為list
                System.out.println("有緩存啦啦啦!!!");
                userList = new ArrayList<>();
                while (iter.hasNext()) {
                //把一個個對象用fastjson格式轉(zhuǎn)換成jsonobject
                    User user = JsonUtils.jsonObjectToUser(iter.next());
                    System.out.println("user   " + user.toString());
                    userList.add(user);
                }
                return JSON.toJSONString(userList);
            } else {
                System.out.println("Toptest沒查過");
                userList = userService.queryTopN();//沒查過就查一次再丟給redis
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
        //如果沒有緩存就批量加入到redis緩存中
            jedisClient.zaddList("Toptest", userList);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return JSON.toJSONString(userList);
    }
這里寫圖片描述

(4)我們緩存策略核心:修改了用戶積分的時候就與緩存排行榜的我們限定的最后一名進(jìn)行比較,大于就丟進(jìn)redis排序,小于就不丟進(jìn)去。所以只有第一次訪問的時候是使用了數(shù)據(jù)庫的sql排序查詢。

@RequestMapping(value = "/updateScore", produces = "text/html;charset=UTF-8", method = {RequestMethod.GET, RequestMethod.GET})
    public String updateScore(Long id,Integer scoreCount) {
        User user = userService.queryById(id);
        scoreService.updateScore(user,scoreCount);
        return JSON.toJSONString(user);
    }

注意的是:因?yàn)槊總€user序列化原因,每個user都有獨(dú)特的標(biāo)記,所以我們要從redis中拿user去判斷,而不是新建一個user。這樣的hash是不一樣的!!!

  @Transactional
    @Override
    public void updateScore(User user, int scoreCount) {
        User userFind = new User();
        Score score = new Score();
        score.setChangeType("做任務(wù)");
        score.setScore(scoreCount);
        score.setUser(user);
        scoreDao.insertScore(score);
        userDao.updateScore(user.getId(), scoreCount);
        //從redis拿出排行榜的list先
        Set<String> userSet = jedisClient.getTopLast("Toptest", (long) 0, (long) 5);
        Iterator<String> iter = userSet.iterator();//遍歷 它,判斷這個用戶存不存在了
        List<User> userList = new ArrayList<>();
        while (iter.hasNext()) {
            User userTemp = JsonUtils.jsonObjectToUser(iter.next());
            System.out.println("user   " + user.toString());
            userList.add(userTemp);
            if (userTemp.getAccount().equals(user.getAccount())) {//如果存在就用存在的
                userFind = userTemp;
            }
        }
        //如果原本有的就修改原有的。
        if (userFind != null) {
            jedisClient.zadd("Toptest", (double) user.getScore(), userFind);
            return;
        }
        //如果沒有就加進(jìn)去嘛,根據(jù)排行榜來區(qū)分
        if (userList.size() >= 20) {
            if (user.getScore() + scoreCount > userList.get(19).getScore()) {
                System.out.println("大于排名20名中的最后一名就丟進(jìn)緩存    :");
                System.out.println("userList    :" + userList.toString());
            }
        } else {
            System.out.println("排行榜小于20人就直接丟進(jìn)緩存    :");
            if (userFind != null) {
                jedisClient.zadd("Toptest", (double) user.getScore(), user);
            }
            System.out.println("user   " + userFind);
        }
    }
這里寫圖片描述

就這樣就完成我們的排行榜緩存策略了!!!


方案二:

只描述:(因?yàn)檫@個方案不是特別優(yōu)雅,而且第一個方案已經(jīng)基于第二方案進(jìn)行改進(jìn)了!!)

(1)用戶訪問排行榜接口,后臺第一次是先查數(shù)據(jù)庫,然后set進(jìn)redis(sorted set或者普通key-value)。

(2)以后:只會訪問redis。

(3)設(shè)置個定時器定時讓redis去讀取數(shù)據(jù)庫topN。(排序在數(shù)據(jù)庫)

ORDERBY 積分 DESC LIMIT 50

排行榜前50名,這樣的條件


源碼下載:Redis系列(二)--緩存設(shè)計(整表緩存以及排行榜緩存方案實(shí)現(xiàn))Demo

好了,J2EE項(xiàng)目系列(四)--Redis系列(二)--緩存設(shè)計(整表緩存以及排行榜緩存方案實(shí)現(xiàn))講完了。本博客系列是我做其中一個項(xiàng)目時候需要整合的部分東西(分布式的往后補(bǔ)上),我把里面的整合,以及部分非算法性的業(yè)務(wù)邏輯寫出來(大家別看這邊博客這么少東西,其實(shí)就那個數(shù)據(jù)庫搞了我?guī)讉€鐘,緩存方案更是我搞了幾天的東東),分享經(jīng)驗(yàn)給大家。歡迎在下面指出錯誤,共同學(xué)習(xí)!!你的點(diǎn)贊是對我最好的支持!!

更多內(nèi)容,可以訪問JackFrost的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容