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.repacket和com.itcrud.redis.vote兩個(gè)包中?。?!
本文作者:程序猿楊鮑
版權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)注明出處