1. 程式人生 > 其它 >Redis學習筆記(三):Redis應用之投票、紅包

Redis學習筆記(三):Redis應用之投票、紅包

技術標籤:Redisredis

Redis基本資料型別及基本命令的使用都已經做完筆記了,接下來就需要將這些筆記實際運用到專案中。經常在專案中用到的就是快取常量資料,還有一些基本的計數等操作,比如我的部落格裡面訪問量、文章閱讀量都是快取在Redis中的,累加閱讀量、訪問量都是在Redis中完成,夜間定時刷入資料庫的,這樣就不用每次訪問都去資料庫中查詢。基本應用沒有問題,那來點稍微複雜的呢,這篇文章就讓我們一起來看看其他的應用場景,將從文章投票排行榜、紅包出發來依次說說具體使用何種資料結構合適。

敘述

在面試的時候經常會被用問Redis用到哪些資料型別,很明顯大多數使用過redis的人都是可以回答上來,但是也僅僅是回答上來幾種資料型別的名字和儲存結構。如果我是面試官,這個回答只能得到5分。為什麼?因為幾種基本資料型別誰都可以說的出來,就是幾個名詞和釋義而已,5分鐘就可以背下來。

如果是我回答我會按這樣的流程來說(個人理解):

  • 5種資料型別,分別是什麼
  • 5種資料型別的儲存結構是什麼樣的,以及儲存特點(str、hash、set、zset、list,zset有分數機制,可排序,set元素不重複,可以做交併差集計算等)
  • 針對不同的儲存特點說明不同資料型別能夠應用在什麼場景下(具體場景細節可以不說,等面試官深挖)

前兩個點應該沒什麼難度。後面的有點難度,特別是沒有怎麼用過redis其他資料型別的,因為很多公司使用redis不會很深。下面簡單介紹一下:

  • string:字串型別,可用來快取文章訪問量、IP訪問量,儲存方便,空間佔用不高,節省記憶體,同時可以通過incr命令來實現自增,不需要繁雜的操作(查詢、修改再插入);也可以用來做常量的快取,對全域性使用的常量資料進行快取,這種常量資料往往是儲存在資料庫中,且被訪問很頻繁,為了降低資料庫的訪問壓力,採用此方式可以更高效。
  • hash:使用的是鍵值的結構,有filed和value,往往可以將filed看成是欄位名,value看成是欄位對應的值,可以用來做物件的儲存,也可以應用在購物車上,記錄當前使用者購物車上的商品資訊。
  • set:相當於Java的set集合,元素不重複是最常用的一個特點,可用來排重,如投票系統,一個人只能給一篇文章投一票,就可以用set集合來記錄當前文章投過票的人員資訊。同時set集合也可以做交併差集的計算(如給使用者定向推送文章,獲取多個使用者的共同愛好,同時批量給多個使用者推送)。
  • zset:有分數機制,可排序,可以用在需要排序的地方,如購物車商品的加入時間排序,投票排行榜的排序等。
  • list:可用來實現佇列,可以用在紅包上面等。

當然這些資料結構應用的場景不止這麼多,可以根據自己專案中實際實用情況調整。

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

文章投票

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

  • 釋出文章
  • 投票
  • 展示投票資訊

釋出文章

//準備的常量資訊、Jedis連線池、發表的文章集合
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<>();

釋出文章的程式碼:

// 發表文章
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");//訪問量
        //儲存文章資訊(使用str)
        resource.hmset(ARTICLE_PREFIX + id, article);
        //將文章加入排行榜中(使用zset)
        resource.zadd(ARTICLE_QUEUE, 0D, ARTICLE_PREFIX + id);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 通過redis字串型別實現ID的自增長操作,incr方法執行後會將當前自增的ID值返回,因為redis是單執行緒的,所以這個ID的自增即使在高併發、分散式系統也是安全可靠的。
  • 儲存文章資訊,採用redis的hash型別儲存,因為文章本身有一個訪問量的屬性,每次被訪問就會做累加,可以使用hincrby命令直接實現。(如果是序列化成JSON,就需要查出來,反序列化成物件,對訪問量累加,再插入,會有兩次連線redis操作和反序列化過程,相對於hash結構,效能和效率都是遜色的)
  • 文章建立後,會將其放在zset中,預設分數是0,每次投票對分數做累加操作

投票

//投票,一篇文章一個人只能投票一次
public static void vote(Long articleId, Long userId) {
    Jedis resource = JEDIS_POOL.getResource();
    try {
        //檢查當前使用者是否已經投過票(使用set)
        Long addResult = resource.sadd(ARTICLE_VOTE + articleId, userId.toString());
        if (addResult == 0) {
            System.out.println("此使用者已為此文章投過票,請勿重複投票!");
            return;
        }
        resource.zincrby(ARTICLE_QUEUE, 1D, ARTICLE_PREFIX + articleId);
        System.out.println(String.format("投票成功,使用者:【%s】,文章:【%s】", userId, articleId));
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}
  • 一個人不能重複給一篇文章投票,那麼就針對每篇文章建立一個set集合,每次投票後,將使用者ID放到對應的set集合中,如果新增成功,累加一票,如果新增失敗,表示已經投過票。
  • 使用set集合的zincrby命令做累加一票的操作。

獲取排行榜資訊

將投票的排行榜資訊展示出來。

//獲取排行榜資訊
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("文章編號:" + tuple.getElement() + ",文章分數:" + tuple.getScore());
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        resource.close();
    }
}

zset的預設排序是正序排列,按照分數從低到高,但是這裡需要看排行榜,就需要使用倒序排列。使用zrevrange命令,同時將具體得分資訊獲取。

模擬釋出和投票過程

模擬釋出文章,然後模擬100個使用者投出500票,包括重複投票,最後將排行榜資訊輸出。

public static void main(String[] args) {
    //建立文章(建立10篇文章,id:1~10)
    for (int i = 1; i <= 10; i++) {
        Map<String, String> article = new HashMap<>();
        article.put("title", "文章標題");
        article.put("content", "文章內容");
        publish(article);
    }
    //投票(隨機100個使用者,總共投500票)
    for (long i = 0; i < 500; i++) {
        vote(artiles.get(new Random().nextInt(10)), (long) new Random().nextInt(100));
    }
    //獲取排行榜
    rank();
}

到這裡一個簡易的投票和排行榜實現過程就結束啦。其中使用到了四種資料型別,分別是set、zset、字串和hash,每種資料型別各司其職,都發揮了自己的優勢。

紅包

看了投票排行榜的套路,紅包也是類似的,選擇合適的資料結構做合適的事。這裡設計的發紅包邏輯簡單一點,就兩個功能點,分別是發紅包和搶紅包。

發紅包

//準備的常量資訊、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";

發紅包實現過程:

//發紅包
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()) {
        //判斷使用者是否已經搶過紅包(使用set)
        Long result = resource.sadd(RED_PACKET_USER, userId.toString());
        if (result == 0) {
            System.out.println("使用者【" + userId + "】已經搶過紅包!");
            return;
        }
        String redpacket = resource.rpop(RED_PACKET_LIST);
        if (StringUtils.isBlank(redpacket)) {
            System.out.println("紅包已經搶完!");
            return;
        }
        System.out.println("恭喜使用者【" + userId + "】搶到紅包,金額:【" + redpacket + "元】");
        //記錄搶紅包的順序
        resource.zadd(RED_PACKET_QUEUE, (double) System.currentTimeMillis(), userId.toString());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

搶紅包之前的雙重判斷,是否已經搶過、紅包是否已經搶完。當搶到紅包後,將紅包和使用者資訊儲存到zset集合中。用時間戳作為分數,用來記錄搶到紅包的先後順序。當然這裡可以使用list佇列來記錄,效果是一樣的。

模擬發紅包和搶紅包過程

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

這裡就發一個紅包,分為5個小紅包,模擬10個使用者去搶,使用多個執行緒來模擬多個使用者。

搶紅包整個實現邏輯使用到3種資料型別,分別是zset、list、set。

上面的兩個示例將5種基本型別都囊括在內了,不同的資料型別根據儲存資料結構、儲存特性的不同,被用來儲存不同的資料。其實不管用在什麼場景下,整體思路是不變的,那就是用合適的資料型別儲存對應的資料。

除了上面的兩個示例,其實還有很多種,比如說購物車,未登陸狀態下加入購物車,登陸後如何將購物車合併到使用者下原有的購物車中,購物車內商品加入的順序,每個商品加入的個數,商品的屬性資訊,購物車有效時間等等。

Source Code

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

兩個示例分別在com.itcrud.redis.repacketcom.itcrud.redis.vote兩個包中!!!