Redis學(xué)習(xí)筆記(三):Redis應(yīng)用之投票、紅包

Redis基本數(shù)據(jù)類(lèi)型及基本命令的使用都已經(jīng)做完筆記了,接下來(lái)就需要將這些筆記實(shí)際運(yùn)用到項(xiàng)目中。經(jīng)常在項(xiàng)目中用到的就是緩存常量數(shù)據(jù),還有一些基本的計(jì)數(shù)等操作,比如我的博客里面訪問(wèn)量、文章閱讀量都是緩存在Redis中的,累加閱讀量、訪問(wèn)量都是在Redis中完成,夜間定時(shí)刷入數(shù)據(jù)庫(kù)的,這樣就不用每次訪問(wèn)都去數(shù)據(jù)庫(kù)中查詢(xún)?;緫?yīng)用沒(méi)有問(wèn)題,那來(lái)點(diǎn)稍微復(fù)雜的呢,這篇文章就讓我們一起來(lái)看看其他的應(yīng)用場(chǎng)景,將從文章投票排行榜、紅包出發(fā)來(lái)依次說(shuō)說(shuō)具體使用何種數(shù)據(jù)結(jié)構(gòu)合適。

敘述

在面試的時(shí)候經(jīng)常會(huì)被用問(wèn)Redis用到哪些數(shù)據(jù)類(lèi)型,很明顯大多數(shù)使用過(guò)redis的人都是可以回答上來(lái),但是也僅僅是回答上來(lái)幾種數(shù)據(jù)類(lèi)型的名字和存儲(chǔ)結(jié)構(gòu)。如果我是面試官,這個(gè)回答只能得到5分。為什么?因?yàn)閹追N基本數(shù)據(jù)類(lèi)型誰(shuí)都可以說(shuō)的出來(lái),就是幾個(gè)名詞和釋義而已,5分鐘就可以背下來(lái)。

如果是我回答我會(huì)按這樣的流程來(lái)說(shuō)(個(gè)人理解):

  • 5種數(shù)據(jù)類(lèi)型,分別是什么
  • 5種數(shù)據(jù)類(lèi)型的存儲(chǔ)結(jié)構(gòu)是什么樣的,以及存儲(chǔ)特點(diǎn)(str、hash、set、zset、list,zset有分?jǐn)?shù)機(jī)制,可排序,set元素不重復(fù),可以做交并差集計(jì)算等)
  • 針對(duì)不同的存儲(chǔ)特點(diǎn)說(shuō)明不同數(shù)據(jù)類(lèi)型能夠應(yīng)用在什么場(chǎng)景下(具體場(chǎng)景細(xì)節(jié)可以不說(shuō),等面試官深挖)

前兩個(gè)點(diǎn)應(yīng)該沒(méi)什么難度。后面的有點(diǎn)難度,特別是沒(méi)有怎么用過(guò)redis其他數(shù)據(jù)類(lèi)型的,因?yàn)楹芏喙臼褂胷edis不會(huì)很深。下面簡(jiǎn)單介紹一下:

  • string:字符串類(lèi)型,可用來(lái)緩存文章訪問(wèn)量、IP訪問(wèn)量,存儲(chǔ)方便,空間占用不高,節(jié)省內(nèi)存,同時(shí)可以通過(guò)incr命令來(lái)實(shí)現(xiàn)自增,不需要繁雜的操作(查詢(xún)、修改再插入);也可以用來(lái)做常量的緩存,對(duì)全局使用的常量數(shù)據(jù)進(jìn)行緩存,這種常量數(shù)據(jù)往往是存儲(chǔ)在數(shù)據(jù)庫(kù)中,且被訪問(wèn)很頻繁,為了降低數(shù)據(jù)庫(kù)的訪問(wèn)壓力,采用此方式可以更高效。
  • hash:使用的是鍵值的結(jié)構(gòu),有filed和value,往往可以將filed看成是字段名,value看成是字段對(duì)應(yīng)的值,可以用來(lái)做對(duì)象的存儲(chǔ),也可以應(yīng)用在購(gòu)物車(chē)上,記錄當(dāng)前用戶(hù)購(gòu)物車(chē)上的商品信息。
  • set:相當(dāng)于Java的set集合,元素不重復(fù)是最常用的一個(gè)特點(diǎn),可用來(lái)排重,如投票系統(tǒng),一個(gè)人只能給一篇文章投一票,就可以用set集合來(lái)記錄當(dāng)前文章投過(guò)票的人員信息。同時(shí)set集合也可以做交并差集的計(jì)算(獲取多個(gè)用戶(hù)的共同愛(ài)好)。
  • zset:有分?jǐn)?shù)機(jī)制,可排序,可以用在需要排序的地方,如購(gòu)物車(chē)商品的加入時(shí)間排序,投票排行榜的排序等。
  • list:可用來(lái)實(shí)現(xiàn)隊(duì)列,可以用在紅包上面等。

當(dāng)然這些數(shù)據(jù)結(jié)構(gòu)應(yīng)用的場(chǎng)景不止這么多,可以根據(jù)自己項(xiàng)目中實(shí)際實(shí)用情況調(diào)整。

這些都介紹完了,下面我們一起看看其在投票排行榜、紅包上的應(yīng)用是怎么做的吧。

文章投票

文章投票主要包含的幾個(gè)功能有以下幾種。

  • 發(fā)布文章
  • 投票
  • 展示投票信息

發(fā)布文章

//準(zhǔn)備的常量信息、Jedis連接池、發(fā)表的文章集合
private static JedisPool JEDIS_POOL = new JedisPool("host", 6379);
private final static String ARTICLE_ID = "article:id:incr";
private final static String ARTICLE_PREFIX = "article:";
private final static String ARTICLE_QUEUE = "article:queue";
private final static String ARTICLE_VOTE = "article:vote:";
public static List<Long> artiles = new ArrayList<>();

發(fā)布文章的代碼:

// 發(fā)表文章
public static void publish(Map<String, String> article) {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //自增生成ID(使用str)
        Long id = resource.incr(ARTICLE_ID);
        artiles.add(id);
        article.put("id", id.toString());
        article.put("viewCount", "0");//訪問(wèn)量
        //存儲(chǔ)文章信息(使用str)
        resource.hmset(ARTICLE_PREFIX + id, article);
        //將文章加入排行榜中(使用zset)
        resource.zadd(ARTICLE_QUEUE, 0D, ARTICLE_PREFIX + id);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 通過(guò)redis字符串類(lèi)型實(shí)現(xiàn)ID的自增長(zhǎng)操作,incr方法執(zhí)行后會(huì)將當(dāng)前自增的ID值返回。
  • 存儲(chǔ)文章信息,采用redis的hash類(lèi)型存儲(chǔ),因?yàn)槲恼卤旧碛幸粋€(gè)訪問(wèn)量的屬性,每次被訪問(wèn)就會(huì)做累加,可以使用hincrby命令直接實(shí)現(xiàn)。(如果是序列化成JSON,就需要查出來(lái),反序列化成對(duì)象,對(duì)訪問(wèn)量累加,再插入,會(huì)有兩次連接redis操作和反序列化過(guò)程,相對(duì)于hash結(jié)構(gòu),性能和效率都是遜色的)
  • 文章創(chuàng)建后,會(huì)將其放在zset中,默認(rèn)分?jǐn)?shù)是0,每次投票對(duì)分?jǐn)?shù)做累加操作

投票

//投票,一篇文章一個(gè)人只能投票一次
public static void vote(Long articleId, Long userId) {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //檢查當(dāng)前用戶(hù)是否已經(jīng)投過(guò)票(使用set)
        Long addResult = resource.sadd(ARTICLE_VOTE + articleId, userId.toString());
        if (addResult == 0) {
            System.out.println("此用戶(hù)已為此文章投過(guò)票,請(qǐng)勿重復(fù)投票!");
            return;
        }
        resource.zincrby(ARTICLE_QUEUE, 1D, ARTICLE_PREFIX + articleId);
        System.out.println(String.format("投票成功,用戶(hù):【%s】,文章:【%s】", userId, articleId));
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 一個(gè)人不能重復(fù)給一篇文章投票,那么就針對(duì)每篇文章創(chuàng)建一個(gè)set集合,每次投票后,將用戶(hù)ID放到對(duì)應(yīng)的set集合中,如果添加成功,累加一票,如果添加失敗,表示已經(jīng)投過(guò)票。
  • 使用set集合的zincrby命令做累加一票的操作。

獲取排行榜信息

將投票的排行榜信息展示出來(lái)。

//獲取排行榜信息
public static void rank() {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //獲取排行榜
        Set<Tuple> tuples = resource.zrevrangeWithScores(ARTICLE_QUEUE, 0L, -1L);
        for (Tuple tuple : tuples) {
            System.out.println("文章編號(hào):" + tuple.getElement() + ",文章分?jǐn)?shù):" + tuple.getScore());
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}

zset的默認(rèn)排序是正序排列,按照分?jǐn)?shù)從低到高,但是這里需要看排行榜,就需要使用倒序排列。使用zrevrange命令,同時(shí)將具體得分信息獲取。

模擬發(fā)布和投票過(guò)程

模擬發(fā)布文章,然后模擬100個(gè)用戶(hù)投出500票,包括重復(fù)投票,最后將排行榜信息輸出。

public static void main(String[] args) {
    //創(chuàng)建文章(創(chuàng)建10篇文章,id:1~10)
    for (int i = 1; i <= 10; i++) {
        Map<String, String> article = new HashMap<>();
        article.put("title", "文章標(biāo)題");
        article.put("content", "文章內(nèi)容");
        publish(article);
    }
    //投票(隨機(jī)100個(gè)用戶(hù),總共投500票)
    for (long i = 0; i < 500; i++) {
        vote(artiles.get(new Random().nextInt(10)), (long) new Random().nextInt(100));
    }
    //獲取排行榜
    rank();
}

到這里一個(gè)簡(jiǎn)易的投票和排行榜實(shí)現(xiàn)過(guò)程就結(jié)束啦。其中使用到了四種數(shù)據(jù)類(lèi)型,分別是set、zset、字符串和hash,每種數(shù)據(jù)類(lèi)型各司其職,都發(fā)揮了自己的優(yōu)勢(shì)。

紅包

看了投票排行榜的套路,紅包也是類(lèi)似的,選擇合適的數(shù)據(jù)結(jié)構(gòu)做合適的事。這里設(shè)計(jì)的發(fā)紅包邏輯簡(jiǎn)單一點(diǎn),就兩個(gè)功能點(diǎn),分別是發(fā)紅包和搶紅包。

發(fā)紅包

//準(zhǔn)備的常量信息、Jedis連接池
private static JedisPool JEDIS_POOL = new JedisPool("host", 6379);
private static final String RED_PACKET_LIST = "redpacket:list";
private static final String RED_PACKET_USER = "redpacket:user";
private static final String RED_PACKET_QUEUE = "redpacket:queue";

發(fā)紅包實(shí)現(xiàn)過(guò)程:

//發(fā)紅包
public static void publish(Integer money) {
    try (Jedis resource = JEDIS_POOL.getResource()) {
        //模擬紅包分配(使用list)
        resource.lpush(RED_PACKET_LIST, 0.13 * money + "",
                0.30 * money + "", 0.23 * money + "", 0.15 * money + "", 0.19 * money + "");
    } catch (Exception e) {
        e.printStackTrace();
    }
}

搶紅包

//搶紅包
public static void rob(Long userId) {
    try (Jedis resource = JEDIS_POOL.getResource()) {
        //判斷用戶(hù)是否已經(jīng)搶過(guò)紅包(使用set)
        Long result = resource.sadd(RED_PACKET_USER, userId.toString());
        if (result == 0) {
            System.out.println("用戶(hù)【" + userId + "】已經(jīng)搶過(guò)紅包!");
            return;
        }
        String redpacket = resource.rpop(RED_PACKET_LIST);
        if (StringUtils.isBlank(redpacket)) {
            System.out.println("紅包已經(jīng)搶完!");
            return;
        }
        System.out.println("恭喜用戶(hù)【" + userId + "】搶到紅包,金額:【" + redpacket + "元】");
        //記錄搶紅包的順序
        resource.zadd(RED_PACKET_QUEUE, (double) System.currentTimeMillis(), userId.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

搶紅包之前的雙重判斷,是否已經(jīng)搶過(guò)、紅包是否已經(jīng)搶完。當(dāng)搶到紅包后,將紅包和用戶(hù)信息存儲(chǔ)到zset集合中。用時(shí)間戳作為分?jǐn)?shù),用來(lái)記錄搶到紅包的先后順序。當(dāng)然這里可以使用list隊(duì)列來(lái)記錄,效果是一樣的。

模擬發(fā)紅包和搶紅包過(guò)程

public static void main(String[] args) {
    //發(fā)紅包
    publish(100);
    //搶紅包(10個(gè)用戶(hù)搶紅包)
    for (int i = 0; i < 10; i++) {
         new Thread(() -> rob((long) new Random().nextInt(10)));
    }
}

這里就發(fā)一個(gè)紅包,分為5個(gè)小紅包,模擬10個(gè)用戶(hù)去搶?zhuān)褂枚鄠€(gè)線程來(lái)模擬多個(gè)用戶(hù)。

搶紅包整個(gè)實(shí)現(xiàn)邏輯使用到3種數(shù)據(jù)類(lèi)型,分別是zset、list、set。

上面的兩個(gè)示例將5中基本類(lèi)型都囊括在內(nèi)了,不同的數(shù)據(jù)類(lèi)型根據(jù)存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)、存儲(chǔ)特性的不同,被用來(lái)存儲(chǔ)不同的數(shù)據(jù)。其實(shí)不管用在什么場(chǎng)景下,整體思路是不變的,那就是用合適的數(shù)據(jù)類(lèi)型存儲(chǔ)對(duì)應(yīng)的數(shù)據(jù)。

除了上面的兩個(gè)示例,其實(shí)還有很多種,比如說(shuō)購(gòu)物車(chē),,未登陸狀態(tài)下加入購(gòu)物車(chē),登陸后如何將購(gòu)物車(chē)合并到用戶(hù)下原有的購(gòu)物車(chē)中,購(gòu)物車(chē)內(nèi)商品加入的順序,每個(gè)商品加入的個(gè)數(shù),商品的屬性信息,購(gòu)物車(chē)有效時(shí)間等等。

Source Code

碼云(gitee):https://gitee.com/itcrud/itcrud-note/tree/master/itcrud-redis

兩個(gè)示例分別在com.itcrud.redis.repacketcom.itcrud.redis.vote兩個(gè)包中?。?!

本文作者:程序猿楊鮑
版權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容