Redis學習筆記(三):Redis應用之投票、紅包
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.repacket
和com.itcrud.redis.vote
兩個包中!!!